Comentário NOIC OBI 2015 - Fase 1 - Programação Nível 2

Comentário por Rogério Júnior

Para ver o caderno de tarefas da primeira fase da Programação Nível 2 da OBI 2015, clique aqui.

Cobra Coral

Note que as cores da coral falsa consistem da repetição de uma sequência de três cores: BVP. Desse modo, em um intervalo de quatro cores, teremos a sequência mostrada anteriormente completa e ainda sobrará uma cor, ou seja, repetiremos a primeira cor. Deste modo, a sequência dada na entrada somente representará uma cobra coral falsa se o primeiro e o último número forem iguais. Por isso, basta que leiamos os quatro números e os salvemos nas variáveis abc e d. Feito isso, se o primeiro e o último números (ad) forem iguais, a coral será falsa e deveremos imprimir uma linha com o caractere 'F' ("if(a==d) printf("F\n");"). Caso contrário, a sequência representará uma coral verdadeira e demos imprimir uma única linha com o caractere 'V' ("else printf("V\n");"). Segue o código comentado.


// Cobra Coral - F1P2 - OBI 2015
// Rogério Júnior
// Complexidade: O(1)
#include <cstdio>
int main(){
// declaro e leio os números da sequência
int a, b, c, d;
scanf("%d %d %d %d", &a, &b, &c, &d);
// se o primeiro for igual ao último
if(a==d) printf("F\n"); // então é uma coral falsa
// caso contrário, é uma coral verdadeira
else printf("V\n");
return 0;
}

view raw

coral.cpp

hosted with ❤ by GitHub

 

Quebra-Cabeça

Cada peça contêm três informações: o número do lado direito, o número do lado esquerdo e o caractere no meio. Desse modo, posso representar o quebra-cabeças como um vetor de pares de nome lista, onde cada par terá um caractere como primeiro elemento e um inteiro como segundo elemento. Desse modo, o o índice de uma posição do vetor será o número esquerdo da peça que ele representa, o primeiro elemento do para será o caractere do meio da peça e o seu segundo elemento  será o número direito da peça. Para usarmos um par, usaremos o objeto já implementado do C++ pair. Se você não o conhece ou não sabe utilizá-lo, clique aqui para ver a aula do Curso Noic sobre estruturas do C++.

Desse modo, ler o quebra cabeça consiste em, para cada peça da entrada, lermos o valores de ec e d de cada peça (número esquerdo, caractere e número direito, respectivamente) e adicionarmos essa peça a lista, salvando, na posição e do vetor, o par (c, d) ("lista[e]=make_pair(c, d);").

Agora precisamos montar o quebra-cabeça. Vamos salvar na variável prox, o número da esquerda da próxima peça que irei imprimir. Assim, ele deve começar com o valor zero e a montagem deve parar quando seu valor for 1, pois ele é o número da direita da última peça. Assim, criaremos um while que irá se repetir enquanto prox for diferente de 1. Em cada repetição do loop, devemos imprimir o caractere da peça cujo número esquerdo é prox. Para isso, devo acessar a posição prox de lista e imprimir o elemento que guarda este caractere do meio, que é o primeiro ("printf("%c", lista[prox].first);"). Feito isso, devo salvar que a próxima peça a ser impressa será a indicada pelo número direito da peça atual. Por isso, prox deve receber o valor do segundo membro (que guarda o número da direita da peça) da peça que acabamos de imprimir (aquela que atualmente está na posição prox) ("prox=lista[prox].second;").

Quando o loop terminar, basta imprimir a quebra de linha. Segue o código comentado:


// Quebra-Cabeça - F1P2 - OBI 2015
// Rogério Júnior
// Complexidade: O(n)
#include <cstdio>
#include <algorithm> // pair e make_pair
using namespace std;
#define MAXN 200200
pair<char,int> lista[MAXN]; // crio o vetor de pares de char e int de nome "lista"
int n, prox; // declaro os inteiros "n" e "prox"
int main(){
scanf("%d", &n); // leio a quantidade de peças
for(int i=1; i<=n; i++){ // para cada peça do quebra-cabeça
// declaro e leio seus valores de "e", "d" e "c"
int e, d;
char c;
scanf(" %d %c %d", &e, &c, &d);
// e adiciono essa peça ao vetor "lista"
lista[e]=make_pair(c, d);
}
// agora basta montar o quebra-cabeça
// que continuará enquanto o número da parte direita da peça atual não for 1
while(prox!=1){ // prox será o número da parte esquerda da peça que irei imprimir
printf("%c", lista[prox].first); // imprimo o caractere dessa peça
// e faço a próxima peça a ser acessada ser a do número direito da peça atual
prox=lista[prox].second;
}
// no fim da saída, imprimo a quebra de linha
printf("\n");
return 0;
}

view raw

quebra.cpp

hosted with ❤ by GitHub

 

Família Real

A família pode ser representada como um grafo direcionado, onde os vértices são os familiares e uma aresta de para u indica que v é o pai de u. Como são n vértices e n-1 arestas em um grafo conexo, então o grafo será uma árvore. Se você não conhece ou não sabe implementar um grafo, clique aqui para ver a aula introdutória do Curso Noic sobre grafos . Vamos salvar o grafo como uma lista de adjacência, em um vetor de vectors de int de nome lista. Vamos declarar os vetores geracao e qtd, que guardarão, respectivamente, a geração, o nível de cada familiar e a quantidade de descendentes em cada geração. O inteiro irá guardar a quantidade de gerações. Agora vamos percorrer o grafo com uma DFS para preenchermos esses vetores com os dados corretos. Se você não conhece ou não sabe utilizar uma DFS, clique aqui para ver a aula do Curso Noic sobre Flood Fill.

A DFS que faremos não precisará marcar os vértices já visitados, visto que nunca chegaremos ao mesmo vértice duas vezes, por o grafo ser uma árvore. Ao invés disso, ela irá receber, também como parâmetro, o nível l, ou seja, a geração em que o vértice está, para marcá-la em geracao e depois adicionar uma unidade em qtd[l]. Além disso, se ela chegar em uma geração mais alta que a maior já encontrada, devemos atualizar o número de gerações h para l. Depois, para cada um dos filhos do vértice, chamo a DFS para eles e aumento o nível em uma unidade.

Depois que todo o grafo for percorrido, basta criarmos o vetor presenca, que irá salvar quantos membros de cada geração da família foram para a festa. Para preenchê-lo, basta lermos os números dos presentes e adicionar uma unidade na presença da geração de cada um deles. Depois de lermos toda a entrada, basta usarmos um for para, em cada geração i, imprimimos o percentual de presença, que será uma double representada pela fórmula 100.00*presenca[i]/qtd[i]. Nao podemos esquecer que a precisão deve ser de duas casas decimais, logo devemos usar o comando "printf("%.2lf  ", 100.00*presenca[i]/qtd[i]);". Veja que é muito importante usarmos 100.00 ao invés de 100 pois, para o C++, a divisão de dois inteiros (presenca[i]/qtd[i]) retorna outro inteiro, mas a presença de uma double (100.00) transforma toda a expressão em uma double. No fim da saída, basta imprimir a quebra de linha. Segue o código comentado:


// Família Real - F1P2 - OBI 2015
// Rogério Júnior
// Complexidade: O(n)
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
#define MAXN 10100
// declaro as variáveis que vou usar
int n, m, h, geracao[MAXN], qtd[MAXN], presenca[MAXN];
// declaro a lista de adjacência que representará o grafo
vector<int> lista[MAXN];
// DFS que usarei para percorrer o grafo
void dfs(int v=0, int l=0){ // ela recebe o vértice e o nível em que estou
geracao[v]=l; // salva que a geração do vértice é o nível
qtd[l]++; // e aumenta em uma unidade a quantidade de descendentes nessa geração
h=max(h, l); // se "l" superar a altura da árvore, então ele é a altura
// para cada um dos filhos de v
for(int i=0; i<lista[v].size(); i++)
dfs(lista[v][i], l+1); // chamarei a DFS para ele, agora um nível abaixo
}
int main(){
scanf(" %d %d", &n, &m); // leio os valores de "n" e "m"
for(int i=1; i<=n; i++){ // para cada um dos descendentes
// declaro e leio quem é seu pai
int p;
scanf(" %d", &p);
// e depois o adiciono ao grafo como filho de seu pai
lista[p].push_back(i);
}
// chamo a DFS para marcar a geração de cada descendente
// bem como a quantidade de descendentes em cada geração
dfs();
// para cada convidado que compareceu à festa
for(int i=1; i<=m; i++){
// declaro e leio seu número
int part;
scanf(" %d", &part);
// e adiciono uma unidade à presença de sua geração
presenca[geracao[part]]++;
}
// depois, para cada geração, imprio o percentual de presença
for(int i=1; i<=h; i++)
// que será 100.00 vezes o quociente entre quantos convidados vieram
// e quantos descendentes compõem cada geração
printf("%.2lf ", 100.0*presenca[i]/qtd[i]);
// por fim, imprimo a quebra de linha
printf("\n");
return 0;
}

view raw

real.cpp

hosted with ❤ by GitHub

 

Caixinha de Palitos

Este problema simples de contagem foi provavelmente o problema mais difícil da prova por se tratar de um algoritmo menos conhecido que os das outras questões. Primeiramente, vamos declarar a variável resp (de valor inicial 0) como long long int, pois a resposta pode superar o valor de um int. A ideia é bem simples: vamos usar um for para simular todas as possíveis quantidades de palitos que o primeiro amigo pode receber (1 a m). Feito isso, veremos qual a menor e a maior quantidade que o segundo amigo pode pegar sem que o que sobre (parte do terceiro amigo) seja maior que m. No fim do for, basta adicionar a resp a quantidade de números compreendidos entre a menor e a maior quantidade que o segundo amigo pode pegar.

Observe agora como implementar a ideia. Na i-ésima repetição do for, vamos declarar o int resto, que será o valor que ainda resta ser dividido entre os dois amigos restantes, após o primeiro amigo ter ficado com palitos. Se esse valor for maior que o dobro de m, então esta quantidade não pode ser dividida entre dois amigos e não devemos fazer nada nesta repetição do loop ("if(resto>2*m) continue;"). Caso contrário vamos declarar os inteiros menor e maior, que são a menor e a maior quantidade de palitos que o segundo amigo pode pegar. Se o segundo amigo pegar menos que resto-m palitos, então o terceiro teria que pegar mais que para que todos os palitos fosse divididos, o que não é permitido, logo menor >= resto-m. Porém, pode ser maior ou igual a resto e, nesse caso, devemos nos lembrar que cada amigo deve receber no mínimo um palito, logo resto >=1. Como essas são as únicas restrições e queremos minimizar o valor de menor, ele receberá o maior valor dentre resto-m, pois não pode ser inferior a nenhum desses dois ("menor=max(1, resto-m);"). O maior número de palitos que o segundo amigo pode pegar é menor ou igual que m, logo maior<=m. Porém, devemos nos lembrar que ele não pode pegar todos os palitos que restam (pois o terceiro amigo deve receber pelo menos um palito), logo maior<=(resto-1). Como essas são as duas únicas restrições e queremos maximizar o valor de maior, ele receberá o menor valor dentre resto-1, pois não pode superar nenhum desses dois ("maior=min(m, resto-1);"). Agora, basta adicionarmos a resp  quantidade de valores entre menor maior, inclusive, que serão exatamente todas as maneiras de o segundo amigo receber palitos, já determinando a quantidade do terceiro ("resp+=maior-menor+1;").

Após o fim deste for, basta imprimirmos o valor de resp, seguido da quebra de linha. Segue o código comentado:


// Caixinha de Palitos - F1P2 - OBI 2015
// Rogério Júnior
// Complexidade: O(m)
#include <cstdio>
#include <algorithm>
using namespace std;
int n, m; // declaro as variáveis "n" e "m"
long long int resp; // declaro resp como long long
int main(){
scanf("%d %d", &n, &m); // leio os valores de "n" e "m"
// para cada uma das possíveis quantidades do primeiro amigo
for(int i=1; i<=m; i++){
int resto=n-i; // vejo quantos palitos ainda sobram
// se for impossível dividí-los entre os dois amigos restantes
// dou continue e não faço mais nada nessa repetição
if(2*m<resto) continue;
// menor será a menor quantidade que o segundo amigo pode ganhar
int menor=max(1, resto-m);
// e maior será a maior quantidade
int maior=min(resto-1, m);
// como a quantidade do terceiro amigo já fica definida
// adiciono a "resp" a quantidade de números no intervalo (menor, maior)
resp+=(maior-menor+1);
}
// e por fim, imprimo o valor de "resp" seguido de quebra de linha
printf("%lld\n", resp);
return 0;
}

view raw

caixinha.cpp

hosted with ❤ by GitHub

 

Banco Inteligente

Este problema é uma pequena variação de um conhecido problema de Programação Dinâmica chamado Coin Change, ou Troco de Moedas, em tradução livre. Se você não conhece Programação Dinâmica ou não sabe como usá-la, clique aqui para ver a aula do Curso Noic sobre esse assunto. Por ser mais intuitiva e simples de implementar, usaremos uma abordagem no estilo Top-Down, em que o estado da DP será determinado por dois parâmetros: int resta, que é o valor que ainda resta ser atingido usando as cédulas e o int tipo, que é quais tipos de notas ainda posso usar para alcançar o valor desejado (todas a partir de tipo).

A recursão é bem simples e muito similar à feita para resolver o problema Troco, da segunda fase da P1 de 2013, com solução do Noic aqui. Já que queremos calcular o número de maneiras de conseguirmos tal valor, este resultado pode ser bem grande e é melhor que a função recursiva retorne um long long int. A função terá o nome dp e a tabela de DP terá o nome tab e começará com todos os estados ainda não calculados com valor igual a -1 (isso será feito com um memset). Como em qualquer recursão, começaremos pelos casos de base. Se o valor que ainda resta ser alcançado é zero, então existe uma única maneira de o atingirmos: não escolhendo nenhuma nota, logo devemos retornar 1 ("if(!resta) return 1LL;"). Se este valor a ser alcançado é negativo ("if(resta<0)"), nunca conseguiremos juntá-lo usando notas, pois todas têm valores positivos. Além disso, se já tivermos olhado todas os seis tipos de notas ("if(tipo>6)"), então não há mais notas que possamos usar e por isso não conseguiremos juntar o valor desejado. Em qualquer um dos dois casos anteriores, não há maneiras de juntarmos o valor desejado, logo a função deve retornar 0 ("return 0LL;"). Além disso, se o estado atual da DP já tiver sido calculado anteriormente, devemos retornar o valor que temos salvo para ele na tabela de DP ("if(tab[resta][tipo]>=0) return tab[resta][tipo];").

Se a função não retornar nos casos base, deveremos utilizar recursão para encontrarmos seu valor. Sejam notas valor os vetores onde estão salvos a quantidade e o valor de cada um dos seis tipos de cédulas diferentes. No atual estado da DP, podemos colocar qualquer quantidade notas do tipo tipo que não ultrapasse a quantidade de notas[tipo] (inclusive nenhuma). Por isso, para calcularmos o valor da função, vamos declarar uma long long int de nome total, que começará com zero e receberá a soma das maneiras de alcançarmos a quantia necessária usando cada uma das possíveis quantidades de cédulas, do tipo que estamos olhando, que temos à disposição. Para isso, usaremos um for para percorrer todas as possíveis quantidades, fazendo um inteiro variar de 0 a notas[tipo]. Desse modo, a quantidade de maneiras de conseguirmos juntar o valor resta usando cédulas de valor valor[tipo] (sem usar tipos anteriores) é a quantidade de maneias de juntarmos o que falta (resta-i*valor[tipo]) usando somente notas que vêm após o tipo atual, ou seja, a partir de tipo+1. Ora! Esse é exatamente o valor de dp(resta-i*valor[tipo], tipo+1). Note ainda que se o valor das  notas do tipo atual ultrapassarem a quantia que resta juntar, podemos parar o for, pois não será possível juntar a quantia e qualquer quantidade de notas maior que também ultrapassará o valor desejado ("if(resta<i*valor[tipo]) break;"). Após  o for, basta que a função retorne o valor de total, salvando este resultado em tab[resta][tipo], para evitar recálculo futuro. Note que, nos casos base, usei "LL" após os valores a serem retornados, para que o fossem na forma de inteiro 64-bit, que é o tipo da função (long long).

Implementada a a função dp, basta lermos o valor s desejado, as quantidades disponíveis de cada nota, usarmos um memset para marcarmos todas as posições de tab com -1 (ainda não calculada) e imprimirmos a quantidade de maneiras de juntarmos valor usando as todas cédulas disponíveis a partir do primeiro tipo, chamando a função dp(n, 1). Segue o código comentado:


// O Banco Inteligente - F1P2 - OBI 2015
// Rogério Júnior
// Complexidade: O(s*n)
// Obs* n é a quantidade média cédulas de cada tipo
#include <cstdio>
#include <cstring>
typedef long long int lli;
#define MAXS 5500
#define MAXN 10
// variáveis globais utilizadas
int s, notas[MAXN], valor[MAXN]={0, 2, 5, 10, 20, 50, 100, 0, 0, 0};
// tabela de DP
lli tab[MAXS][MAXN];
// função recursiva da DP Top-Down, que recebe, como parâmetros
// o valor a ser alcançado e o tipo de nota que estamos olhando
lli dp(int resta, int tipo=1){
// se o valor desejado é zero, existe uma maneira de conseguí-lo:
// não usar nota nenhum. Retorno 1
if(!resta) return 1LL;
// se passarmos do tipo permitido de nota ou procurarmos valor negativo
// então não é possível alcançarmos tal valor desejado
if(tipo>6 or resta<0) return 0LL;
// se o atual estado da DP já foi calculado antes
// retorno o que tenho salvo na tabela de DP
if(tab[resta][tipo]>=0) return tab[resta][tipo];
//se afunção não retornou, preciso chamar a recursão para calculá-la
// declaro a "lli total" que começará com zero
// e será o número de maneiras que está sendo procurado
lli total=0LL;
// para cada uma das possíveis quantidades de cédulas do tipo que estou olhando
for(int i=0; i<=notas[tipo]; i++){
// se o valor dessas cédulas ultrapassar o que resta, paro o loop
if(resta<i*valor[tipo]) break;
// caso contrário, adiciono ao total o número de maneiras de atingir
// o valor que falta, usando somente as cédulas que faltam
total+=dp(resta-i*valor[tipo], tipo+1);
}
// depois retorno o valor de total, e o salvo na tabela de DP
return tab[resta][tipo]=total;
}
int main(){
// leio o valor desejado
scanf("%d", &s);
// leio as quantidades de cada cédula
for(int i=1; i<7; i++) scanf("%d", &notas[i]);
// faço todas as posições da tabela de DP começarem com valor -1
memset(tab, -1, sizeof(tab));
// e imprimo a quantidade de maneiras calculado pela função recursiva
// para juntar valor "s" olhando as cédulas a partir da primeira
printf("%lld\n", dp(s)); // obs* note que chamo a dp(s, 1)
return 0;
}

view raw

banco.cpp

hosted with ❤ by GitHub

 

Se você ainda tiver alguma dúvida em algum dos problemas, vá para a página inicial do Curso Noic de Informática e preencha o formulário para nos enviar sua dúvida.