Os Grafos e o Orkut

Vou neste e nos próximos artigos falar-lhes sobre a Teoria dos Grafos. É uma coisa que poderia ser complicada, então pra facilitar o entendimento eu resolvi que trabalharemos sempre com exemplos da “vida real”. Neste artigo, vamos trabalhar com o Orkut como base, partindo do princípio que todo mundo sabe o que é o Orkut.

Neste primeiro artigo, só falarei sobre a definição do grafo e sua utilidade. Apresentarei as definições de: vértice, aresta, grau, grafo orientado, grau de entrada e grau de saída. Então, vamos lá!

A definição de grafo é muito simples. Segundo o Professor Cid Carvalho de Souza: Uma forma de representar uma relação binária entre elementos de um conjunto. Bom… É simplesmente uma representação de relações (que chamamos de arestas) entre objetos, que chamamos de vértices. Vamos logo ao exemplo: a amizade no Orkut.

Um grafo de amizades

Estão vendo esta árvore? Esta é a representação que chamamos de grafo. Vamos supor que este é o grafo das amizades do Orkut e as bolinhas nele são as seguintes pessoas:

  1. Eu
  2. João
  3. Maria
  4. José
  5. Pedro

Eu (número 1) tenho dois amigos: o João (número 2) e a Maria (número 3), porque estou ligado a eles. O João (número 2) tem três amigos: eu (número 1), a Maria (número 3) e o José (número 4). E assim podemos fazer com os outros contando as linhas que saem de cada pessoa.

Cada pessoa é um vértice e cada linha (relação entre duas pessoas) é uma aresta. Dizemos que é uma relação binária lá em cima, porque a relação é sempre entre dois vértices.

Os números de amigos que cada pessoa tem (o número de relações que cada vértice tem) é o que chamamos de “grau” de um vértice. Grau dos vértices do exemplo acima:

  1. 2
  2. 3
  3. 2
  4. 2
  5. 1

Pra quê serve o grafo?

Ora, se você consegue contar as arestas que saem de cada vértice na programação (se você sabe fazer algo básico como calcular o grau de um vértice), você pode oferecer seus serviços ao Google e ganhar milhões, pois, como todos sabem, o Orkut não sabe fazer isso direito!

Brincadeiras a parte… Grafos são extremamente úteis porque são representações bastante simples (você teve dificuldade para entender minha árvore ali em cima?) e essa estrutura deles aparece em muitos problemas computacionais. Além disso, existem muitos algoritmos eficientes para problemas complexos que utilizam estas representações.

Definições até agora

Traduzindo os conceitos do Orkut para os grafos:

  • Vértice: Pessoa.
  • Aresta: Amizade entre uma pessoa e outra.
  • Grau de um vértice: Número de amigos de uma pessoa.

Grafos Orientados

Um grafo é orientado quando eu sou seu amigo, mas você não é meu amigo. Você sabe que um grafo é orientado através da representação quando ele tem “setinhas”, como o grafo abaixo.

Grafo orientado

Vamos supor que isso aí é um mapa do Brasil. Ele despreza as cidades que não são importantes para o país, levando apenas em consideração portanto: São Paulo, Florianópolis e Itajaí.

  • São Paulo é a bolinha vermelha.
  • Florianópolis é a bolinha amarela.
  • Itajaí é a bolinha azul.

As arestas indicam que há uma estrada para você ir de uma cidade para a outra, mas só dá pra ir no sentido da estrada, que as setas representam. Portanto, você pode ir de São Paulo a Florianópolis, de São Paulo a Itajaí e Florianópolis a Itajaí, mas não de Itajaí para qualquer outro lugar.

Grau dos Vértices

Os graus dos vértices neste segundo grafo seriam os seguintes, certo?

  • São Paulo: 2
  • Florianópolis: 2
  • Itajaí: 2

Quase… Porém, nos grafos orientados nós temos dois tipos de grau diferentes. Dizemos que:

  • grau de entrada é o número de arestas que apontam para o vértice; e
  • grau de saída é o número de arestas que saem do vértice.

Portanto, os graus de entrada do nosso grafo são:

  • São Paulo: 0
  • Florianópolis: 1
  • Itajaí: 2

E os graus de saída:

  • São Paulo: 2
  • Florianópolis: 1
  • Itajaí: 0

Onde mais posso utilizar grafos?

Existem vários casos onde você vai querer usar grafos:

  • Mapas
  • Sua árvore genealógica
  • Hierarquia de uma empresa
  • Sistema de amizades do seu sistema de comunidades virtuais
  • … e muito mais. Na OBI já apareceu até um jogo de dominó como problema de grafos!

Como veremos nos próximos artigos, tem algoritmo pra fazer “tudo” em grafos… Então representar alguma coisa em grafos é muito útil pra poder descobrir uma série de coisas.

A maioria das páginas que eu conheço sobre grafos são muito malignas porque apresentam uns 50 conceitos diferentes de grafos juntos (ex.: grafo conexo, grafo desconexo, caminho, ciclo, etc.). Nos meus artigos, devo tratar um assunto de cada vez para facilitar o entendimento. Então, vou parar por aqui hoje.

No próximo artigo: Como representar um grafo na programação? Você já pode ir pensando nisso…

Mini-Poker

Resolvi fazer uma pausa nos algoritmos de ordenação para mostrar como podemos usar os conhecimentos já adquiridos de maneira prática. Vamos neste artigo resolver o problema Mini-Poker, que caiu na prova da Programação Nível 2 (categoria para pessoas até 19 anos ou primeiro ano da faculdade) da Olimpíada Brasileira de Informática de 2005.

Esse post ficou gigante, mas é muito simples. Leia com atenção e acho que você não terá problemas… ;)

Objetivos

Com esta resolução de problema, espero treinar com vocês o conceito de:

  • Interpretação do Probema
  • Entrada e Saída
  • Ordenação por Inserção
  • Pseudocódigo

Acho que será legal para pôrmos em prática o que já estudamos sobre algoritmos.

O problema é bem simples, mas é só pra iniciar.

Enunciado

Mini-Poker é o nome do jogo de cartas que é uma simplificação de Poker, um dos mais famosos jogos de cartas do mundo. Mini-Poker é jogado com um baralho normal de 52 cartas, com quatro naipes (copas, paus, espadas e ouro), cada naipe compreendendo treze cartas (Ás, 2, 3, 4, 5, 6, 7, 8, 9, 10, Valete, Dama, Rei).

No início do jogo, cada jogador recebe cinco cartas. O conjunto de cinco cartas vale um certo número de pontos, de acordo com as regras descritas abaixo. Diferentemente do jogo de Poker normal, em Mini-poker o naipe das cartas é desconsiderado. Assim, para simplificar a descrição do jogo, vamos utilizar os números de 1 a 13 para identificar as cartas do baralho, na ordem dada acima. Uma outra diferença é que pode ocorrer empate entre mais de um vencedor; nesse caso os vencedores dividem o prêmio.

As regras para pontuação em Mini-Poker são as seguintes:

  1. Se as cinco cartas estão em seqüência a partir da carta xx (ou seja, os valores das cartas são xx, x+1x+1, x+2x+2, x+3x+3 e x+4x+4), a pontuação é x+200x+200 pontos. Por exemplo, se as cartas recebidas são 10, 9, 8, 11 e 12, a pontuação é 208 pontos.
  2. Se há quatro cartas iguais xx (uma quadra, ou seja, os valores das cartas são xx, xx, xx, xx e yy), a pontuação é x+180x+180 pontos. Por exemplo, se as cartas recebidas são 1, 1, 1, 10 e 1, a pontuação é 181 pontos.
  3. Se há três cartas iguais xx e outras duas cartas iguais yy (uma trinca e um par, ou seja, os valores das cartas são xx, xx, xx, yy e yy), a pontuação é x+160x+160 pontos. Por exemplo, se as cartas recebidas são 10, 4, 4, 10 e 4, a pontuação é 164 pontos.
  4. Se há três cartas iguais xx e duas outras cartas diferentes yy e zz (uma trinca, ou seja, os valores das cartas são xx, xx, xx, yy e zz), a pontuação é x+140x+140 pontos. Por exemplo, se as cartas recebidas são 2, 3, 2, 2 e 13, a pontuação é 142 pontos.
  5. Se há duas cartas iguais xx, duas outras cartas iguais yy (xyx \neq{} y) e uma outra carta distinta zz (dois pares, ou seja, os valores das cartas são xx, xx, yy, yy e zz), a pontuação é 3×x+2×y+203 \times{} x + 2 \times{} y + 20 pontos, em que x>yx > y. Por exemplo, se as cartas recebidas são 12, 7, 12, 8 e 7, a pontuação é 70 pontos.
  6. Se há apenas duas cartas iguais xx e as outras são distintas (um par, ou seja, os valores das cartas são xx, xx, yy, zz e tt), a pontuação é xx pontos. Por exemplo, se as cartas recebidas são 12, 13, 5, 8 e 13, a pontuação é 13 pontos.
  7. Se todas as cartas são distintas, não há pontuação.

Tarefa

Escreva um programa que, fornecidas as cartas dadas a um jogador, calcule a pontuação do jogador naquela jogada.

Entrada

A entrada é composta por vários casos de teste, cada um correspondendo a uma jogada. A primeira linha da entrada contém um número inteiro NN que indica o número de casos de teste (1N1001 \leq{} N \leq{} 100). Cada uma das NN linhas seguintes contém cinco números inteiros C_1C\_{1}, C_2C\_{2}, C_3C\_{3}, C_4C\_{4} e C_5C\_{5}, representando as cinco cartas recebidas por um jogador (1C_1,C_2,C_3,C_4,C_5131 \leq{} C\_{1}, C\_{2}, C\_{3}, C\_{4}, C\_{5} \leq{} 13).

A entrada deve ser lida do dispositivo de entrada padrão (normalmente o teclado).

Saída

Para cada caso de teste da entrada, seu programa deve produzir três linhas na saída. A primeira linha deve conter um identificador do caso de teste, no formato “Teste n”, onde n é numerado seqüencialmente a partir de 1. A segunda linha deve conter a pontuação do jogador considerando as cinco cartas recebidas. A terceira linha deve ser deixada em branco. A grafia mostrada no Exemplo de Saída, abaixo, deve ser seguida rigorosamente.

A saída deve ser escrita no dispositivo de saída padrã (normalmente a tela).

Restrições

1N1001 \leq{} N \leq{} 100 1C_1,C_2,C_3,C_4,C_5131 \leq{} C\_{1}, C\_{2}, C\_{3}, C\_{4}, C\_{5} \leq{} 13

Exemplo de Entrada

2
12 3 10 3 12
1 2 3 5 4

Saída para o Exemplo de Entrada

Teste 1
62

Teste 2
201

Comentários sobre os problemas de olimpíadas

Todos os problemas passados em competições de programação tem um enunciado parecido com o desse. São especificados todos os limites (restrições), é dito exatamente como será a entrada e como deve ser a saída e geralmente tem uma historinha no começo… :D

Bom… Todos esses dados são fundamentais. Alguns limites nem vamos usar, não tem importância para a nossa solução, mas pode ter importância para outra pessoa que queira implementar um algoritmo diferente. A sintaxe da entrada e da saída são extremamente importantes. Na prova da Seletiva IOI do ano passado, eu quase perdi 60 pontos (6 casos de teste) na solução de um problema simples porque meu programa desprezava um espaço no início de uma frase quando imprimia uma saída. E mesmo a historinha do começo é fundamental. Ela sempre dá boas dicas e algumas vezes até ilustra o problema (às vezes a gente nem lê o enunciado e já sabe que é um problema de grafos!)

Mas vamos a solução deste problema…

Por onde começar?

Com o tempo você pode decidir fazer um caminho diferente, mas eu sugiro começar sempre pelo recebimento da entrada. Aliás, acho que isto é atípico, porque a maioria das pessoas prefere ler bastante o problema e desenvolver todo o algoritmo a mão antes de botar a mão na massa. Eu acho que depois que a gente recebe a entrada, fica bem mais fácil fazer o resto e a gente pode ir pensando enquanto a gente recebe a entrada! Então, depois que lemos o problema e já entendemos tudo o que ele quer, vamos fazer a entrada!

O problema fala que começa nos dando um número N que será o número de casos de teste que teremos que receber depois. Sem dificuldade podemos escrever o _pseudo_código a seguir:

recebe N
para nteste \leftarrow{} 1 até N, faça
fim-para

Já chamo a variável que loopa como nteste, porque já li a saída do problema e sei que vou precisar imprimir o número de caad caso de teste… ;)

Aí o enunciado diz que Cada uma das NN linhas seguintes contém cinco números inteiros C_1C\_{1}, C_2C\_{2}, C_3C\_{3}, C_4C\_{4} e C_5C\_{5}, representando as cinco cartas recebidas por um jogador (1C_1,C_2,C_3,C_4,C_5131 \leq{} C\_{1}, C\_{2}, C\_{3}, C\_{4}, C\_{5} \leq{} 13). Então, vamos receber os cinco números em cada iteração e colocá-los num vetor, é claro!

recebe N
para nteste \leftarrow{} 1 até N, faça
recebe C1,C2,C3,C4,C5C_{1}, C_{2}, C_{3}, C_{4}, C_{5}
fim-para

E a entrada está pronta.

Desenvolvimento

O programa se baseia em encontrarmos valores iguais nos elementos do vetor. O que podemos fazer para facilitar essa tarefa?

Isso mesmo: A ordenação! :D Se os elementos estiverem ordenados, ficará bem mais fácil para procurarmos quatro números iguais, porque eles não poderão ser qualquer uma das possibilidades, mas somente C1,C2,C3,C4C_{1}, C_{2}, C_{3}, C_{4} ou C2,C3,C4,C5C_{2}, C_{3}, C_{4}, C_{5}.

Aí que algoritmos devemos implementar para ordenar? Isso é uma conclusão que vamos chegar no final de nossa série, mas para este algoritmo não tem solução melhor que a Ordenação por Inserção. É um caso pequeno (n=5) e a Ordenação por Inserção é mais rápida que a por Seleção, porque o seu melhor caso é uma função linear. Então, vamos implementar o Insertion Sort no nosso algoritmo:

recebe N
para nteste \leftarrow{} 1 até N, faça
recebe C1,C2,C3,C4,C5C_{1}, C_{2}, C_{3}, C_{4}, C_{5}
// início da ordenação por inserção
para j \leftarrow{} 2 até 5
  elemento \leftarrow{} CjC_{j}
  i \leftarrow{} j-1
  enquanto i > 0 e CiC_{i} > elemento, faça
   Ci+1C_{i+1} \leftarrow{} CiC_{i}
   ii \leftarrow{} Ci1C_{i-1}
  fim-enquanto
  Ci+1C_{i+1} \leftarrow{} elemento
fim-para
// fim da ordenação por inserção
fim-para

O bom desses algoritmos de ordenação é que sua lógica é muito simples e por isso é fácil decorá-los… Ao menos o Insertion Sort e o Selection Sort são algoritmos básicos que todo programador deve conhecer bem. Bom… Acredito que vocês não tenham tido dificuldade pra entender até aqui. A cor vermelha no pseudocódigo eu vou usar daqui pra frente para um comentário, que aliás, é uma excelente prática de boa programação.

O resto do problema precisa calcular quantos pontos o cara fez, baseado em suas cartas, agora já ordenadas. Para isto vamos criar uma função para testar vários se e retornar o resultado.

Eu poderia tirar os se aninhados, mas assim fica mais fácil a compreensão.

Como vamos ver com os pseudocódigos a seguir, é fácil testar cada uma das regras com o vetor ordenado:

Primeira Regra – Seqüência

Se as cinco cartas estão em seqüência a partir da carta xx(ou seja, os valores das cartas são xx, x+1x+1, x+2x+2, x+3x+3 e x+4x+4), a pontuação é x+200x+200 pontos. Por exemplo, se as cartas recebidas são 10, 9, 8, 11 e 12, a pontuação é 208 pontos.

se C1=C21C_{1} = C_{2}-1 e C2=C31C_{2} = C_{3}-1 e C3=C41C_{3}=C_{4}-1 e C4=C51C_{4}=C_{5}-1, então
retorna C1+200C_{1}+200
fim-se

Segunda Regra – Quadra

Se há quatro cartas iguais xx (uma quadra, ou seja, os valores das cartas são xx, xx, xx, xx e yy), a pontuação é x+180x+180 pontos. Por exemplo, se as cartas recebidas são 1, 1, 1, 10 e 1, a pontuação é 181 pontos.

se C1=C2=C3=C4C_{1} = C_{2} = C_{3} = C_{4} ou C2=C3=C4=C5C_{2} = C_{3} = C_{4} = C_{5}, então
retorna C2+180C_{2}+180
fim-se

Aqui retornamos C_2C\_{2} porque ele será sempre parte da quadra (ela começando em C_1C\_{1} ou C_2C\_{2}).

Terceira e Quarta Regra – Trinca

Se há três cartas iguais xx e outras duas cartas iguais yy (uma trinca e um par, ou seja, os valores das cartas são xx, xx, xx, yy e yy), a pontuação é x+160x+160 pontos. Por exemplo, se as cartas recebidas são 10, 4, 4, 10 e 4, a pontuação é 164 pontos.

se C1=C2=C3C_{1} = C_{2} = C_{3} ou C2=C3=C4C_{2} = C_{3} = C_{4} ou C3=C4=C5C_{3} = C_{4} = C_{5}, então
se ( C1C3C_{1} \neq{} C_{3} e C1=C2C_{1} = C_{2} ) ou ( C3C5C_{3} \neq{} C_{5} e C4=C5C_{4} = C_{5} ), então
  retorna C3+160C_{3}+160

Se há três cartas iguais xx e duas outras cartas diferentes yy e zz (uma trinca, ou seja, os valores das cartas são xx, xx, xx, yy e zz), a pontuação é x+140x+140 pontos. Por exemplo, se as cartas recebidas são 2, 3, 2, 2 e 13, a pontuação é 142 pontos.

senão
  retorna C3+140C_{3} + 140
fim-se
fim-se

Note que aqui retornamos C_3C\_{3} porque ele será sempre parte da trinca (o mesmo motivo que retornarmos C_2C\_{2} para a quadra).

Quinta Regra – Duas Duplas

Se há duas cartas iguais xx, duas outras cartas iguais yy (xyx \neq{} y) e uma outra carta distinta zz (dois pares, ou seja, os valores das cartas são xx, xx, yy, yy e zz), a pontuação é 3×x+2×y+203 \times{} x + 2 \times{} y + 20 pontos, em que x>yx > y. Por exemplo, se as cartas recebidas são 12, 7, 12, 8 e 7, a pontuação é 70 pontos.

se C1=C2C_{1} = C_{2} ou C2=C3C_{2} = C_{3}, então
se C3=C4C_{3} = C_{4} ou C4=C5C_{4} = C_{5}, então
  retorna 3×C4+2×C2+203 \times{} C_{4} + 2 \times{} C_{2} + 20
fim-se
fim-se

C_2C\_{2} será sempre elemento da menor dupla e C_4C\_{4} será sempre elemento da maior dupla. Por isso usamos eles como yy e xx, respectivamente.

Sexta Regra – Dupla

Se há apenas duas cartas iguais xx e as outras são distintas (um par, ou seja, os valores das cartas são xx, xx, yy, zz e tt), a pontuação é xx pontos. Por exemplo, se as cartas recebidas são 12, 13, 5, 8 e 13, a pontuação é 13 pontos.

se C1=C2C_{1} = C_{2} ou C2=C3C_{2} = C_{3}, então
retorna C2C_{2}
senão se C3=C4C_{3} = C_{4} ou C4=C5C_{4} = C_{5}, então
retorna C4C_{4}
fim-se

Separei em dois SEs porque senão não saberíamos que valor retornar.

Sétima Regra

Se todas as cartas são distintas, não há pontuação.

retorna 0

Função Inteira

Juntando todos os SEs, temos:

função pontua (C)
// primeira regra
se C1=C21C_{1} = C_{2}-1 e C2=C31C_{2} = C_{3}-1 e C3=C41C_{3}=C_{4}-1 e C4=C51C_{4}=C_{5}-1, então
  retorna C1+200C_{1}+200
fim-se

// segunda regra
se C1=C2=C3=C4C_{1} = C_{2} = C_{3} = C_{4} ou C2=C3=C4=C5C_{2} = C_{3} = C_{4} = C_{5}, então
  retorna C2+180C_{2}+180
fim-se

//terceira e quarta regra
se C1=C2=C3C_{1} = C_{2} = C_{3} ou C2=C3=C4C_{2} = C_{3} = C_{4} ou C3=C4=C5C_{3} = C_{4} = C_{5}, então
  se ( C1C3C_{1} \neq{} C_{3} e C1=C2C_{1} = C_{2} ) ou ( C3C5C_{3} \neq{} C_{5} e C4=C5C_{4} = C_{5} ), então
   retorna C3+160C_{3}+160
  senão
   retorna C3+140C_{3} + 140
  fim-se
fim-se

// quinta regra
se C1=C2C_{1} = C_{2} ou C2=C3C_{2} = C_{3}, então
  se C3=C4C_{3} = C_{4} ou C4=C5C_{4} = C_{5}, então
   retorna 3×C4+2×C2+203 \times{} C_{4} + 2 \times{} C_{2} + 20
  fim-se
fim-se

// sexta regra
se C1=C2C_{1} = C_{2} ou C2=C3C_{2} = C_{3}, então
  retorna C2C_{2}
senão se C3=C4C_{3} = C_{4} ou C4=C5C_{4} = C_{5},
então
  retorna C4C_{4}
fim-se

// sétima regra
retorna 0
fim-função

Já que a função retorna assim que encontra um resultado, não há risco de ocorrer nada errado (por exemplo, uma quadra é sempre uma trinca, que é sempre uma dupla). Agora basta colocarmos esta função no nosso código e adaptar para a saída ser igual a que o problema pede.

Saída

Para chegar a saída, basta fazermos o programa imprimir Teste nteste e depois o retorno da função pontua. Com isto, temos:

recebe N
para nteste \leftarrow{} 1 até N, faça
recebe C1,C2,C3,C4,C5C_{1}, C_{2}, C_{3}, C_{4}, C_{5}
// início da ordenação por inserção
para j \leftarrow{} 2 até 5
  elemento \leftarrow{} CjC_{j}
  i \leftarrow{} j-1
  enquanto i > 0 e CiC_{i} > elemento, faça
   Ci+1C_{i+1} \leftarrow{} CiC_{i}
   ii \leftarrow{} Ci1C_{i-1}
  fim-enquanto
  Ci+1C_{i+1} \leftarrow{} elemento
fim-para
// fim da ordenação por inserção

imprime “Teste ”
imprime linha testen
imprime linha pontua(C)
imprime linha
fim-para

Fiz essa saída assim pra se parecer com Pascal, mas para cada linguagem ela pode ser bem diferente… Vejamos dois exemplos…

C

printf("Teste %d\n%d\n\n", nteste, pontua(C));

PHP

echo "Teste ".$nteste."\n".pontua($C)."\n\n";

Comentários sobre o problema

Este problema é muito chato. É trivial, mas perdemos um tempo enorme escrevendo ses. Ninguém gosta de um problema como esse, mas quando cai numa olimpíada somos obrigados a resolver… hehehe… Mas, para a felicidade geral de todos, saibam que a maioria dos problemas de olimpíadas não são assim. Exigem mais lógica e menos código. Com o tempo, vamos pegando problemas mais difíceis. Espero só ter cumprido meu objetivo dando uma utilidade pra ordenação, entrada e saída e que vocês tenham entendido tudo.

Sugiro que quem esteja aprendendo algoritmos com meus artigos e já saiba programar um pouquinho, resolva alguns problemas simples do site da OBI, que separei especialmente pra vocês!

E, gostaria de fixar, mais importante é a interpretação e o seu pensamento… Programar é fácil!

Quanto lixo!

Impressionante a quantidade de besteiras que todo programador faz… Às vezes, uma semana depois de fazer um programa ou um site, eu já sinto raiva do script que acabei de fazer e me sinto obrigado a refazê-lo. Brincando um pouco nas férias, estou refazendo vários problemas da OBI e cada vez mais percebo a quantidade de lixo que achamos nos nossos scripts. E o pior é perceber o tempo que eu levava pra fazer aqueles problemas que podiam ser resolvidos de maneira tão simples (e eu pensava que tinha uma solução muito boa)… Estou resolvendo a lista de tarefas da modalidade Programação Nível 2, mas apenas os problemas de grafos (todos eles eu já tinha resolvido, mas estou agora programando-os melhor). Confiram as besteiras que eu fiz nos primeiros deles:

Aeroporto

[enunciado]

Um problema de grafos? Não! Mas parece muito. Na verdade, se ele pedisse qualquer coisa mais do que o grau de cada vértice eu precisaria de representar usando grafos, mas a única coisa que ele quer é que eu conte a quantidade de vezes que cada número aparece na entrada.

A primeira solução deste problema, que agora já não está mais entre nós, foi feita no curso de programação básica da OBI 2004, em Campinas, quando começava a aprender grafos. Pra vocês terem uma idéia do drama, eu fiz uma busca em profundidade pra contar o número de arestas que cada vértice tem (pra medir o grau de cada vértice).

Confiram a básica solução que fiz ontem: (e que daqui a algum tempo posso vir a achar ridícula também… hehehe)

#include <stdio.h>
#define AMAX 101

int main() {
	int a, v, x, y, t[AMAX]; // t = tráfego, grau dos vértices
	int i, maior, teste=1;

	while (scanf("%d %d", &a, &v)&&a&&v) {
		maior=0;
		for (i=1; i<=a; i++) {
			t[i]=0;
		}
		for (i=0; i<v; i++) {
			scanf("%d %d", &x, &y);
			t[x]++;
			t[y]++;
			if (t[x]>maior) {
				maior=t[x];
			}
			if (t[y]>maior) {
				maior=t[y];
			}
		}
		printf("Teste %dn", teste++);
		for (i=1; i<=a; i++) {
			if (t[i]==maior) {
				printf("%d ", i);
			}
		}
		printf("bnn");
	}

	return 0;
}

Batuíra

[enunciado]

O objetivo é achar o caminho mínimo de peso de 1 a N. Uma simples busca em profundidade resolve o problema. Agora vejam a busca em profundidade que eu faria em 2004, que ainda não tirei da minha galeria de códigos: batuira.c e comparem com a que eu fiz ontem (e que ainda poderia ser melhorada):

#include <stdio.h>
#include <values.h>
#define NMAX 101

int n, marc[NMAX], g[NMAX][NMAX];
int resultado;

void buscaemprofundidade(int v, int soma) {
	int w;

	if (v==n) {
		if (soma<resultado) {
			resultado=soma;
		}
	} else {
		marc[v]=1;
		for (w=1; w<=n; w++) {
			if (g[v][w]&&!marc[w]) {
				buscaemprofundidade(w, soma+g[v][w]);
			}
		}
	}
}

int main() {
	int x, y, xy;
	int i, j, teste=1;

	while (scanf("%d", &n)&&n) {
		resultado=MAXINT;
		for (i=1; i<=n; i++) {
			marc[i]=0;
			for (j=1; j<=n; j++) {
				g[i][j]=0;
			}
		}
		while (scanf("%d %d %d", &x, &y, &xy)&&x&&y&&xy) {
			g[x][y]=xy;
			g[y][x]=xy;
		}
		buscaemprofundidade(1, 0);
		printf("Teste %dn%dnn", teste++, resultado);
	}

	return 0;
}

Dengue

[enunciado]

Esse foi com certeza meu maior susto. Foi por causa desse problema que eu resolvi escrever este artigo… Dá até vergonha de mostrar a busca em profundidade que usei para resolver o problema anteriormente. O objetivo do problema é descobrir partindo de que vértice do grafo o vértice que se encontra mais longe tem custo menor. Ou seja, é só fazer uma busca em largura com todos os vértices. Mas antigamente eu não simpatizava muito com a busca em largura, então fiz aquela besteira. E imaginem quanto tempo eu não levei pra fazer aquela joça… Bom… Pelo menos deve ter servido pra eu quebrar a cabeça naquela época! Vejam o código novo (sujeito a mudanças, é claro!):

#include <stdio.h>
#include <values.h>
#define NMAX 101

int main() {
	int w, i, j, x, y, teste=1, g[NMAX][NMAX], n, d[NMAX], fim, ini, fila[NMAX], v, a, md[NMAX], c;

	while (scanf("%d", &n)&&n) {
		for (i=1; i<=n; i++) {
			for (j=1; j<=n; j++) {
				g[i][j]=0;
			}
		}
		for (i=1; i<n; i++) {
			scanf("%d %d", &x, &y);
			g[x][y]=1;
			g[y][x]=1;
		}
		c=0;
		md[0]=MAXINT;
		for (v=1; v<=n; v++) {
			for (i=1; i<=n; i++) {
				d[i]=n;
			}
			md[v]=0;
			d[v]=0;
			ini=0;
			fim=0;
			fila[fim++]=v;
			while (ini!=fim) {
				a=fila[ini++];
				if (d[a]>md[v]) {
					md[v]=d[a];
				}
				for (w=1; w<=n; w++) {
					if (g[a][w]&&d[w]==n) {
						d[w]=d[a]+1;
						fila[fim++]=w;
					}
				}
			}
			if (md[v]<md[c]) {
				c=v;
			}
		}
		printf("Teste %dn%dnn", teste++, c);
	}

	return 0;
}

Bom… Simplificando… Se você não é programador, não seja; você vai ficar louco! :lol: Este problema que citei aqui não acontece só com esses problemas de olimpíadas mas também com vários scripts, principalmente os que vamos alterando com o tempo e adicionando novas features. Já recomecei do zero muitos sites para deixá-los decentes e muitos programas também (essa versão do aeroporto.c já é a terceira!) e não só esses de olimpíadas (o meu programa de ouvir música, em Bash, eu já fiz umas 10 vezes).

Quando eu acabar de re-resolver todos os problemas da seção de códigos lógicos eu vou publicar todos juntos. Por enquanto, vou deixar tudo do jeito que tá pra vocês apreciarem meus scripts mal-feitos. ;)


Quem costuma visitar meu blog perceberá que apareceu um ícone lá no canto inferior direito, escrito Bom Demais para o IE. A imagem, posicionada lá embaixo usando um position:fixed; (que o IE não suporta) é de uma campanha muito legal que você pode conhecer clicando no link. Participem e tenham um site “bom demais para o Internet Explorer”! :)

Palíndromos Primos

Fiquei um bom tempo sem fazer o treinamento da USACO, porque há algum tempo tinha parado no programa Prime Palindromes, cujo objetivo é listar todos os palíndromos primos entre dois números (limites: 5, 10.000.000).

Esta demora aconteceu porque eu, além de ter ficado muito tempo sem entrar na USACO e já ter me esquecido do problema, estava testando todos os números, vendo se eles eram palíndromos, depois primos e então imprimia. Quando eu entrei na USACO essa semana (idéia do César Kawakami, que também vai pra UNICAMP mês que vem e foi um cara que também me ajudou nesse problema) vi que tinham Hints que eu nunca tinha visto antes. E elas diziam que eu devia gerar palíndromos. Com isso ficou fácil…

Eu ainda boiei um pouco, porque só depois eu descobri uma coisa lógica e muito simples (que eu nunca tinha pensado antes): Para descobrir se um número N qualquer é primo, basta ver se ele é divisível pelos primos (no caso, eu usei todos os números, não só primos) de 2 a raiz de N. Bom, isso é bem óbvio… Mas ninguém nunca tinha me dito e eu nunca tinha visto em lugar nenhum! Então tive que pensar (descobrir sozinho mesmo).

Prime Palindromes

Por preguiça de só fazer alguns for caso o mínimo fosse menor que X e maior que Y, meu programa, para qualquer caso, pega todos os palíndromos primos de 5 a 10000000! :blink: Eu não sabia se o tempo disso ia ser suficiente, então resolvi testar assim antes de fazer esses ifs antes do for e deu certo! Logo, nem precisa mais de nada… O tempo do meu programa para qualquer teste, no meu Linux, é 0,032 segundos. Na USACO apareceu como 0,05 segundos.

Código-fonte

//Prime Palindromes - USACO Training Gateway
//Tiago Madeira (c)

//Agora eu sei que dá pra fazer com custo bem menor,
//mas esse aí rolou na boa com 0.05 segundos.

/*
ID: contato1
PROG: pprime
LANG: C
*/

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>

int eh_primo(long int num) {
	int i;

	for (i=3; i*i<=num; i+=2) {
		if (!(num%i)) {
			return 0;
		}
	}

	return 1;
}

int main() {
	int i, j, k, l, cont=0;
	long int numero, min, max, v[10000];

	FILE *in=fopen("pprime.in", "r");
	FILE *out=fopen("pprime.out", "w");
	fscanf(in, "%d %d", &min, &max);
	fclose(in);

	v[cont++]=5;
	v[cont++]=7;
	for (i=1; i<=9; i+=2) {
		numero=i*10+i;
		if (eh_primo(numero)) {
			v[cont++]=numero;
		}
	}
	for (i=1; i<=9; i+=2) {
		for (j=0; j<=9; j++) {
			numero=i*100+j*10+i;
			if (eh_primo(numero)) {
				v[cont++]=numero;
			}
		}
	}
	for (i=1; i<=9; i+=2) {
		for (j=0; j<=9; j++) {
			for (k=0; k<=9; k++) {
				numero=i*10000+j*1000+k*100+j*10+i;
				if (eh_primo(numero)) {
					v[cont++]=numero;
				}
			}
		}
	}
	for (i=1; i<=9; i+=2) {
		if (i==5) {
			i=7;
		}
		for (j=0; j<=9; j++) {
			for (k=0; k<=9; k++) {
				for (l=0; l<=9; l++) {
					numero=i*1000000+j*100000+k*10000+l*1000+k*100+j*10+i;
					if (eh_primo(numero)) {
						v[cont++]=numero;
					}
				}
			}
		}
	}
	for (i=0; i<cont; i++) {
		if (v[i]>=min&&v[i]<=max) {
			fprintf(out, "%d\n", v[i]);
		} else if (v[i]>max) {
			fclose(out);
			return 0;
		}
	}
	fclose(out);
	return 0;
}

Agora vou prosseguir com o treinamento do USACO Training Gateway na seção 1.3, a começar pelo problema Mixing Milk.

O Homem que Calculava

Nos últimos dias não aconteceu nada demais. Só fiquei emocionado por ter recebido um 9,1 em biologia… :lol: E outra coisa legal também é que eu reli O Homem que Calculava e achei muito legal. Eu tinha lido na sexta série e acho que não tinha entendido direito tudo. O livro é muito bom e não é muito complicado não. O próximo que eu quero reler é O Diabo dos Números. Esse é mais “avançado” que o primeiro. Tô fazendo um trabalho de escola (de história) sobre (o filósofo) Pitágoras. É bem legal, o cara era muito bom. Na verdade, o trabalho tá virando de matemática, mas é bem interessante. É legal ter um professor de história que dá aula… ;) Não é igual ano passado, né? Tô achando bem legal os períodos da Grécia Antiga.

Ah, e vou finalizar citando um trecho d’O Homem que Calculava em homenagem ao Vavá, que não respira oxigênio… :D

Conta-se que o famoso rei Salomão, para demonstrar a finura e a sabedoria de seu espírito, deu à sua noiva, a rainha de Sabá – a famosa Belquiss – uma caixa com 529 pérolas. Por que 529? Sabe-se que 529 é o quadrado de 23, isto é, 529 é igual a 23 multiplicado por 23. E 23 era, exatamente, a idade da rainha.

Solução dos Problemas da OBI2005

Agora que eu acho que todas as escolas já submeteram as soluções dos problemas da Programação Nível 1 da OBI desse ano, estou publicando minhas quatro soluções, em C.

Frota de Táxi

//Frota de Taxi - OBI2005
#include <stdio.h>

int main() {
	float a, g, ra, rg, al, ga;

	scanf("%f %f %f %f", &a, &g, &ra, &rg);
	al=a/ra;
	ga=g/rg;

	if (al<ga) {
		printf("A\n");
	} else {
		printf("G\n");
	}

	return 0;
}

Campo de Minhocas

//Campo de Minhocas - OBI2005
#include <stdio.h>
#define NMAX 102
#define NMAX 102

int main() {
	int n, m, soma, i, j, matriz[NMAX][NMAX], maior=0;

	scanf("%d %d", &n, &m);

	for (i=1; i<=n; i++) {
		for (j=1; j<=m; j++) {
			scanf("%d", &matriz[i][j]);
		}
	}

	for (i=1; i<=n; i++) {
		soma=0;
		for (j=1; j<=m; j++) {
			soma+=matriz[i][j];
		}
		if (soma>maior) {
			maior=soma;
		}
	}

	for (i=1; i<=m; i++) {
		soma=0;
		for (j=1; j<=n; j++) {
			soma+=matriz[j][i];
		}
		if (soma>maior) {
			maior=soma;
		}
	}

	printf("%d\n", maior);

	return 0;
}

Duende Perdido

//Duende Perdido - OBI2005
#include <stdio.h>
#include <values.h>
#define NMAX 102

int menor=MAXINT, p[NMAX][NMAX], custo[NMAX][NMAX];

int duende(int x, int y, int cus, int ox, int oy) {

	if (p[x][y]==1||p[x][y]==3) {
		if (cus<custo[x][y]) {
			custo[x][y]=cus;
			if ((x!=ox||y+1!=oy)&&p[x][y+1]!=-1) {
				duende(x, y+1, cus+1, x, y);
			}
			if ((x+1!=ox||y!=oy)&&p[x+1][y]!=-1) {
				duende(x+1, y, cus+1, x, y);
			}
			if ((x!=ox||y-1!=oy)&&p[x][y-1]!=-1) {
				duende(x, y-1, cus+1, x, y);
			}
			if ((x-1!=ox||y!=oy)&&p[x-1][y]!=-1) {
				duende(x-1, y, cus+1, x, y);
			}
		}
	}
	if (p[x][y]==0) {
		if (cus<custo[x][y]) {
			custo[x][y]=cus;
		}
		if (custo[x][y]<menor) {
			menor=custo[x][y];
		}
	}
}

int main() {
	int n, m, ix, iy, i, j;

	scanf("%d %d", &n, &m);

	for (i=0; i<=n+1; i++) {
		for (j=0; j<=m+1; j++) {
			p[i][j]=-1;
			custo[i][j]=MAXINT;
		}
	}

	for (i=1; i<=n; i++) {
		for (j=1; j<=m; j++) {
			scanf("%d", &p[i][j]);
			if (p[i][j]==3) {
				ix=i;
				iy=j;
			}
		}
	}

	duende(ix, iy, 0, 0, 0);

	printf("%d\n", menor);

	return 0;
}

Trilhas

//Trilhas - OBI2005
#include <stdio.h>
#include <values.h>
#define NMAX 102
#define MMAX 1001

int main() {
	int campeao, n, m[NMAX], a[NMAX][MMAX], saida=0, i, j, parou, subir[NMAX], descer[NMAX], menor=MAXINT;

	scanf("%d", &n);
	for (i=1; i<=n; i++) {
		scanf("%d", &m[i]);
		for (j=1; j<=m[i]; j++) {
			scanf("%d", &a[i][j]);
		}
	}

	//Primeiro vamos ver se precisa haver esforço de subida
	for (i=1; i<=n; i++) {
		parou=0;
		for (j=1; j<=m[i]; j++) {
			//printf("%d %d\n", i, j);
			if (a[i][j]>a[i][j+1]&&j!=m[i]) {
				parou=1;
				//printf("parou!\n");
				j=m[i];
			}
		}
		if (!parou) {
			//Então vamos parar por aí...
			printf("%d\n", i);
			return 0;
		}
	}

	//A mesma coisa ao contrário
	for (i=1; i<=n; i++) {
		parou=0;
		for (j=m[i]; j>=1; j--) {
			//printf("%d %d\n", i, j);
			if (a[i][j]>a[i][j-1]&&j!=1) {
				parou=1;
				//printf("parou!\n");
				j=1;
			}
		}
		if (!parou) {
			//Então vamos parar por aí...
			printf("%d\n", i);
			return 0;
		}
	}

	//Não deu...
	//Vamos contar quantos metros vamos ter que subir (ou descer=subir ao contrário)
	for (i=1; i<=n; i++) {
		descer[i]=0;
		subir[i]=0;
		for (j=1; j<=m[i]; j++) {
			if (a[i][j]>a[i][j+1]) {
				descer[i]+=(a[i][j]-a[i][j+1]);
			} else {
				subir[i]+=(a[i][j+1]-a[i][j]);
			}
		}
	}

	//E quem sobe ou desce menos?
	for (i=1; i<=n; i++) {
		if (subir[i]<menor) {
			menor=subir[i];
		}
		if (descer[i]<menor) {
			menor=descer[i];
		}
	}

	//Mas peraí... Temos que ver o primeiro na ordem de identificação!
	for (i=1; i<=n; i++) {
		if (subir[i]==menor||descer[i]==menor) {
			printf("%d\n", i);
			return 0;
		}
	}

	return 0;
}

O último deles (Trilhas) tá meio problemático. Tá fazendo um monte de coisa que não precisava… :S É que deu uns problemas lá na hora e eu tava cansado por causa do Duende e daí tive lag pra interpretar o enunciado e já que o tempo tava acabando por causa de problemas com o computador onde tava fazendo a prova eu fiz de uma maneira bem precária! Acho que ele tá com erros…

O do Duende é o mais interessante. Os outros dois não tiveram muita graça. O Trilhas também era bem fácil, mas inesperadamente o meu cérebro deu um Segmentation Fault quando fui fazer ele.

Gostaria que se alguém achasse algum erro em algum dos scripts me avisasse. O pessoal da OBI ainda não divulgou um gabarito pra testar os programas e nem o resultado. Acredito que semana que vem deve sair alguma coisa…

© 2005–2020 Tiago Madeira