Comentário NOIC OBI 2016 - 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 2016, clique aqui.

Lâmpadas do hotel

Conhecimento prévio necessário:

  1. Entrada e saída (Aula 1)
  2. Estruturas condicionais (Aula 2)

Vamos ler a entrada e guardá-la nos inteiros iaibfa fb. Vamos declarar também um inteiro resp, que guardará a resposta. Observe que só há uma maneira de alterar a lâmpada B: usando o interruptor 2, logo, se ib for diferente de fb, então devemos usar o interruptor 2, logo adicionamos um a resp e trocamos o estado da lâmpada A.

Feito isso, se os estados inicial e final da lâmpada A forem diferentes, então ainda precisamos apertar o interruptor 1 uma vez, logo adicionamos mais um a resp. Por fim, basta imprimir o valor de resp. Segue o código para melhor entendimento:


// Lâmpadas do hotel - F1P2 - OBI 2016
// Rogério Júnior
// Complexidade: O(1)
#include <cstdio>
int ia, ib, fa, fb, resp;
int main(){
scanf("%d %d %d %d", &ia, &ib, &fa, &fb);
// se os estados inicial e final de B forem diferentes
if(fb!=ib){
// aperto o interruptor 2
// então aumento 1 em resp
resp++;
// e troco o estado da lâmpada A
ia=(!ia);
}
// se os estados inicial e final de A forem diferentes
if(ia!=fa) resp++; // aumento 1 em resp
// por fim, imprimo o valor de resp
printf("%d\n", resp);
return 0;
}

view raw

hotel.cpp

hosted with ❤ by GitHub

Chaves

Conhecimento prévio necessário:

  1. Entrada e saída (Aula 1)
  2. Estruturas condicionais (Aula 2)

Note que, neste problema, os únicos caracteres que nos interessam são as chaves ('{' e '}'), logo, enquanto houver entrada, vamos ler cada um dos caracteres dela. Note que o número dado no começo não interfere pois ele não contêm nenhuma chave. Vamos salvar um inteiro qtd que guarda quantas chaves estão abertas no momento e uma variável booleana erro, que começa como falsa e fica verdadeira se em algum momento fecharmos uma chave que não foi previamente aberta.

Para cada caractere lido, se ele for uma chave abrindo ('{'), vamos apenas incrementar o valor de qtd em uma unidade. Se, entretanto, for uma chave fechando ('}'), vamos antes checar se há chaves abertas, pois se não houver, haverá erro de compilação no código ("if(qtd<1) erro=true;").

Após lermos a entrada, só precisamos checar duas coisas: se em algum momento fechamos uma chave que não havia sido aberta, erro será verdadeira e haverá erro de compilação. Além disso, se ainda houver chaves abertas, então qtd será diferente de zero e, novamente, haverá erro de compilação. Caso nenhum dos dois ocorra, não há problema nas chaves do código. Segue o código para melhor entendimento:


// Chaves - F1P2 - OBI 2016
// Rogério Júnior
// Complexidade: O(entrada)
#include <cstdio>
int qtd;
bool erro;
char c;
int main(){
// leio todos os caracteres da entrada
while(scanf("%c", &c)!=EOF){
// se for uma chave abrindo
if(c=='{') qtd++; // incremento o valor de qtd em 1 unidade
// se for uma chave fechando
if(c=='}'){
// checo se há chaves abertas
if(qtd<1) erro=true;
// e diminuo o valor de qtd em 1 unidade
qtd--;
}
}
// se fechei chaves que não haviam sido abertas ou ainda há chaves abertas
if(erro||qtd) printf("N\n"); // há erro de compilação
// caso contrário, não há erro nas chaves do código
else printf("S\n");
return 0;
}

view raw

chaves.cpp

hosted with ❤ by GitHub

Chuva

Conhecimento prévio necessário:

  1. Entrada e saída (Aula 1)
  2. Estruturas condicionais (Aula 2)
  3. Estruturas de repetição (Aula 2)
  4. Vetores (Aula 3)

Observe uma seção i qualquer da piscina, com altura h_i. Note que, se existem j e k tal que j<i<k e h_j>h_i e h_k>h_i, então a seção i da piscina ficará coberta de água, pois há paredes mais altas para acumulá-la. Deste modo, seja prefix_x a maior altura entre h_1, h_2, h_3, ..., h_x e suffix_x a maior entre h_x, h_{x+1}, h_{x+2}, ..., h_n. Deste modo, para saber se há seções da piscina anteriores e posteriores a i que são mais altas que h_i, basta checarmos se h_i<prefix_{i-1} e h_i<suffix_{i+1}.

Para fazermos isso de maneira rápida, basta precalcularmos os valores de prefix_i e suffix_i para todo i e salvarmos em um vetor. Depois iremos percorrer todas as alturas (que também estarão salvas em um vetor) e verificaremos se cada seção estará coberta de água. Para cada seção coberta incrementaremos o valor da resposta em uma unidade. Para o precálculo, basta percorrermos todas as alturas observando a seguinte recursão: prefix_0=0 e prefix_i=max(h_i,prefix_{i-1}) \forall i\geq 1. De maneira análoga, suffix_{n+1}=0 e suffix_i=max(h_i,suffix{i+1}) \forall i\leq n.

Por fim, basta imprimirmos o valor da resposta. Segue o código para melhor entendimento:


// Chuva - F1P2 - OBI 2016
// Rogério Júnior
// Complexidade: O(n)
#include <cstdio>
#include <algorithm>
using namespace std;
#define MAXN 100100
int n, prefix[MAXN], suffix[MAXN], h[MAXN], resp;
int main(){
// leio o valor de n e das alturas
scanf("%d", &n);
for(int i=1;i<=n;i++) scanf("%d", &h[i]);
// precaulculo os valores de prefix e suffix com a recursão mostrada
for(int i=1;i<=n;i++) prefix[i]=max(prefix[i-1],h[i]);
for(int i=n;i>=1;i--) suffix[i]=max(suffix[i+1],h[i]);
// para cada seção da piscina
for(int i=1;i<=n;i++)
// se ela estiver debaixo d'água
if(h[i]<min(prefix[i-1],suffix[i+1]))
resp++;// incremento o valor de resp
// por fim, imprimo o valor salvo em resp
printf("%d\n", resp);
return 0;
}

view raw

chuva.cpp

hosted with ❤ by GitHub

Toca do Saci

Conhecimento prévio necessário:

  1. Flood Fill (Aula 9)

Pela descrição do problema, há um único caminho que leva da casa 2 à casa 3. Logo, basta encontrarmos as casas com número 2 e 3 no tabuleiro e executarmos uma DFS de origem na casa 3, calculando as distâncias como calcularíamos em uma árvore (a distância até u é a distância até seu pai v mais 1. Segue o código para melhor entendimento:


// Toca do Saci - F1P2 - OBI 2016
// Rogério Júnior
// Complexidade: O(n*m)
#include <cstdio>
#include <cstring> // memset
#define MAXN 1010
int n, m, tab[MAXN][MAXN], vx[4]={0,1,0,-1}, vy[4]={1,0,-1,0}, x, y, x_f, y_f, dist[MAXN][MAXN];
// DFS que encontrará as distâncias
void dfs(int i, int j){
// para cada possibilidade de deslocamento
// (cima, baixo, esquerda e direita)
for(int k=0;k<4;k++){
// vejo a casa para a qual iria
int i_=i+vx[k], j_=j+vy[k];
// se ela estiver dentro da matriz
// e não tiver sido alcançada ainda
if(tab[i_][j_]>0 && dist[i_][j_]<0){
// atualizo sua distância
dist[i_][j_]=dist[i][j]+1;
// e chamo a DFS nela
dfs(i_,j_);
}
}
}
int main(){
memset(dist,-1,sizeof dist); // defino todas as distâncias como -1
// leio os valores de n e m
scanf("%d %d", &n, &m);
// leio os valores da matriz
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
scanf("%d", &tab[i][j]);
// se encontrei a casa 2, salvo os valores de x e y
if(tab[i][j]==2){
x=i;
y=j;
}
// se encontrei a casa 3, salvo os valores de x_f e y_f
if(tab[i][j]==3){
x_f=i;
y_f=j;
}
}
// a distância da casa 2 para ela mesma será definida com um
// pois o problema a conta no tamanho do caminho
dist[x][y]=1;
// chamo a DFS na casa inicial
dfs(x,y);
// imprimo a distância que a DFS encontrou até a saída
printf("%d\n", dist[x_f][y_f]);
return 0;
}

view raw

toca.cpp

hosted with ❤ by GitHub

Sanduíche

Conhecimento prévio necessário:

  1. Estruturas de repetição (Aula 2)
  2. Vetores (Aula 3)

Note, primeiramente, que se o tamanho de todo o sanduíche não alcançar o valor de D, não há como satisfazer às exigências do problema, logo, imprimimos zero e fechamos o programa.

Agora, vamos reduzir o problema a algo mais simples: note que se duplicarmos o sanduíche (C_{i+N}=C_i \forall i \leq n), podemos ignorar a segunda operação (a de retirar das pontas), pois, no caso, retirar os segmentos de 1 a C_i e de C_j a N seria o mesmo que retirar o segmento contínuo de C_j a C_{i+N}. Note ainda, que só precisamos testar todos os intervalos que começam em i \leq N, pois qualquer intervalo (C_i,C_j), N<i<j pode ser representado no intervalo (C_{i-N},C_{j-N}).

Basta agora procurarmos os modos de escolhermos um sequência contínua de pedaços no sanduíche duplicado que tenha tamanho D. Para cada i, vamos encontrar o valor f_i de modo que ele seja o fim da maior sequência contínua de pedaços que começa no pedaço i e cujo tamanho não supera D (\sum\limits_{k=i}^{f_i}C_k \leq D e \sum\limits_{k=i}^{f_i+1}C_k > D). Note que f_i é único que, se existe j tal que \sum\limits_{k=i}^{j}C_k =D, então j=f_i. Note também que, se j>i, então f_j \geq f_i, pois \sum\limits_{k=i}^{f_i}C_k \leq D \Rightarrow\sum\limits_{k=i}^{j-1}C_k+\sum\limits_{k=j}^{f_i}C_k \leq D \Rightarrow\sum\limits_{k=j}^{f_i}C_k \leq D \Rightarrow f_i \leq f_j.

Deste modo, para cada i \leq N, basta encontrarmos f_i e testarmos se a soma do segmento de i a f_i é exatamente D. Se for, incrementamos a resposta em uma unidade. Seja prefix_i =\sum\limits_{k=1}^{i}C_k. Deste modo, a soma do intervalo contínuo (i,j) é exatamente prefix_j-prefix_{i-1}.

Assim, uma possível solução quadrática para o problema seria, para cada i, procurarmos dentre todos os j tal que  2N \geq j \geq i o f_i, incrementando o valor de j enquanto prefix_j-prefix_{i-1} \leq D. Quando enfim encontrarmos j=f_i, bastaria verificar se a soma do intervalo contínuo de i a f_i é D. Se fosse incrementaríamos 1 na resposta.


for(int i=1;i<=n;i++)
for(int j=i;j<2*n && prefix[j+1]-prefix[i-1]<=d){
j++;
if(prefix[j]-prefix[i-1]==d) resp++;
}

Porém, estamos esquecendo o fato provado de que f_{i+1} \geq f_i, logo não precisamos procurar f_i em todos os possíveis j tal que i \leq j \leq 2N, mas apenas entre os valores maiores ou iguais a f_i, ou seja, 0s j tal que f_i \leq j \leq 2N. Para isso, basta declararmos o j fora do segundo for para que ele continue com seu valor salvo, e quando calcularmos o valor de f_{i+1} já começarmos de f_i.


int j=1;
for(int i=1;i<=n;i++)
while(j<2*n && prefix[j+1]-prefix[i-1]<=d) j++;
if(prefix[j]-prefix[i-1]==d) resp++;

Note que a complexidade deste loop é linear, pois alteramos o valor de i no máximo N vezes e o valor de j no máximo 2N vezes. Por fim, basta imprimirmos a resposta. Segue o código para melhor entendimento:


// Sanduíche - F1P2 - OBI 2016
// Rogério Júnior
// Complexidade: O(n)
#include <cstdio>
#define MAXN 1000100
int n, d, v[2*MAXN], prefix[2*MAXN], resp, fim;
int main(){
scanf("%d %d", &n, &d);
// leio os tamanho dos pedaços
for(int i=1;i<=n;i++){
scanf("%d", &v[i]);
// sempre lembrando de duplicar o sanduíche
v[i+n]=v[i];
}
// precalculo as somas de todos os prefixos
for(int i=1;i<=2*n;i++) prefix[i]=v[i]+prefix[i-1];
// se o tamanho total do sanduíche for menor que d
if(prefix[n]<d){
// não há solução
printf("0\n");
return 0;
}
// caso contrário, vamos procurar os intervalos com soma D
// o fim de cada intervalo começa como 1
fim=1;
// e para cada início
for(int ini=1;ini<=n;ini++){
// incremento o fim enquanto puder
while(fim<2*n && prefix[fim+1]-prefix[ini-1]<=d) fim++;
// e testo se a soma do intervalo é D
if(prefix[fim]-prefix[ini-1]==d) resp++;
}
// por fim, imprimo o valor salvo em resp
printf("%d\n", resp);
return 0;
}

view raw

sanduiche.cpp

hosted with ❤ by GitHub