Comentário Noic OBI 2020 – Fase 1 Turno A – Programação Nível 2

Comentário por Pedro Racchetti e Luca Dantas

Para conferir a prova na íntegra, clique aqui.

Acelerador de Partículas

Conhecimento Prévio Necessário:


Para a resolução dessa questão é importante conhecer o uso do operador de módulo, que em C++ é representado por $$\%$$, ou também pode ser feito utilizando a estrutura de repetição while já que os limites do problema são baixos. Porém, utilizaremos aqui o operador de módulo, pois é a solução adequada e mais eficiente.

O primeiro fator a ser percebido é que não importa onde a partícula termine, ela vai terá que passar por três seções antes de entrar no ciclo, e também, depois de sair do ciclo, terá que passar por mais duas. Então, após lermos a distância a ser percorrida no início, podemos subtrair $$5$$ do valor total e assim reduzimos o problema a encontrar qual o ponto em que a partícula sairá do ciclo.

É fácil perceber que enquanto a distância a ser percorrida dentro do ciclo for maior ou igual a $$8$$ (tamanho do ciclo) a partícula poderá simplesmente dar uma volta completa e voltar para o ponto anterior com $$8$$ quilômetros a menos a serem precorridos. Desse modo, o que queremos encontrar é a quantidade de quilômetros restantes após todos esses ciclos já terem sido contabilizados. É possível perceber que:

  • Se $$1$$ quilômetro restar, o ponto final será o sensor $$1$$.
  • Se $$2$$ quilômetros restarem, o ponto final será o sensor $$2$$
  • Se $$3$$ quilômetros restarem, o ponto final será o sensor $$3$$

Lembre-se de confirmar os pontos acima na imagem disponibilizada na questão. Assim, para retirarmos essa repetição dos ciclos, basta tirarmos a distância a ser percorrida em módulo $$8$$ ($$a \% b$$ ou “$$a$$ módulo $$b$$” é igual ao resto da divisão de $$a$$ por $$b$$).

Assim, a complexidade final da solução é $$O(1)$$. Segue o código abaixo para melhor compreensão:

 

Promoção de Primeira:

Conhecimentos prévios necessários:

A primeira observação importante é perceber que o reino de linearlândia pode ser representado como uma árvore, já que temos $$N$$ vértices e $$N-1$$ arestas e todos os vértices estão conectados por pelo menos um caminho. (Perceba que também consequentemente não há ciclos).

Podemos nos valer dessas propriedades de árvore e “enraizá-la” em um vértice qualquer e assim buscar a resposta para cada sub-árvore enraizada. Caso o conceito de “enraizar” uma árvore não seja familiar, observe a imagem: nela o vértice $$1$$ está se comportando como a raiz da árvore, e também é importante perceber que, como o grafo é uma árvore, é garantido que cada uma das sub-árvores formadas pelos filhos da raíz (no caso do desenho as subárvores formadas por $$3$$, $$4$$ e $$5$$) nunca se conectam, pois por definição uma árvore não possui ciclos e existir alguma ligação entre essas sub-árvores implicaria um ciclo. Essa observação segue recursivamente para todos os filhos (no caso do desenho é possivel perceber que a sub-árvore formada por $$5$$ segue as mesmas propriedades e implicações da árvore formada com raiz em $$1$$).

Antes de entrar na explicação da ideia trataremos cada linha de ônibus como uma cor para a aresta, sendo preto para a RoyalBus e branco para a ImperialBus.

Tendo essa definição em sub-árvores, podemos utilizar um conceito de programação dinâmica e nos perguntar o que realmente importa em cada uma das sub-árvores que poderia ser útil para calcular a resposta da sub-árvore atual. Defina $$maior_{branco}[u]$$ como o maior caminho que sai do nó $$u$$, termina na subárvore de $$u$$ e possui arestas de cores alternadas, onde a primeira aresta saindo de $$u$$ possui cor branca. Defina $$maior_{preto}[u]$$ analogamente. Podemos chegar no seguinte lema:

Lema: se o caminho da resposta estiver totalmente contido na sub-árvore de um nó $$u$$ e passar por este nó, esse caminho será composto pela união de dois outros caminhos:

  • O caminho representado em $$maior_{branco}[u]$$
  • O caminho representado em $$maior_{preto}[u]$$

O lema acima vale devido ao fato de que o caminho que passa por $$u$$ deverá ter arestas consecutivas com cores alternadas. Logo, é sempre ótimo escolhermos os caminhos de tamanho máximo para as duas cores (branco e preto).

Assim, precisamos encontrar, para cada vértice $$u$$, os valores $$maior_{branco}$$ e $$maior_{preto}$$. Podemos encontrá-los utilizando uma ideia similar a Programação Dinâmica, realizando uma $$DFS$$ na árvore. A resposta final do problema é, assim, o valor máximo de $$maior_{branco}[u] + maior_{preto}[u] + 1$$ dentre todos os vértices $$u$$ da árvore.

A complexidade final da solução é $$O(n)$$. Confira o código abaixo para melhor entendimento:

Fissura Perigosa

Conhecimento prévio necessário:

Para resolver esse problema podemos usar uma DFS ou BFS (no código abaixo foi utilizada a DFS) para realizarmos um flood fill na matriz, partindo da posição do canto superior esquerdo. Além disso, precisamos garantir que, ao passar de uma posição a outra, essa nova posição não tem altitude maior que a força inicial. Outro possível caso de erros acontece quando a primeira posição tem altitude maior que a força, e o modo de entrada, já que a matriz é dada como uma série de $$strings$$ de números.

Complexidade: $$O(N^2)$$

Segue o código, comentado, para melhor compreensão:


#include<bits/stdc++.h>
using namespace std;
int orig[512][512]; //matriz original, dada na entrada.
int dx[5] = {1, 0, -1, 0};
int dy[5] = {0, 1, 0, -1}; //vetores de auxílio, que servirão como
//arestas no grafo.
int n, f;
void dfs(int i, int j){
orig[i][j] = 100;
//definimos a posição i j como 100, valor absurdo
//para que, no momento de saída, se reconheca o fato que é de fato uma
//posição atingida pela lava
for(int k = 0; k < 4; k++){
int ii = i + dx[k];
int jj = j + dy[k];
if(ii <= 0 || ii > n || jj <= 0 || j > n) continue;
if(orig[ii][jj] > f) continue;
dfs(ii, jj);
}
}
int main(){
scanf("%d %d", &n, &f);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
char c;
scanf(" %c", &c);
//lemos cada posição como um caractér, e depois os transformamos
//em um inteiro
int k = c – '0';
orig[i][j] = k;
}
}
if(orig[1][1] <= f){
dfs(1, 1);
//se a posicao inicial tiver altitude menor que a forca, iniciamos a DFS
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
if(orig[i][j] > 9){
//todos os poontos atingidos estao marcados como 100
printf("*");
}else{
printf("%d",orig[i][j]);
}
}
printf("\n");
}
}

view raw

fissura.cpp

hosted with ❤ by GitHub

Pandemia

Conhecimento prévio necessário:

Para esse problema, podemos primeiro perceber que qualquer reunião que tenha acontecido antes do primeiro amigo ser infectado, não influenciará na resposta, portanto podemos ignorá-las. Iremos guardar as reuniões em um vetor de $$vectors$$, onde cada $$vector$$ desse vetor guarda os amigos que estiveram presentes na reunião. Além disso, iremos usar um vetor de marcação para representar os amigos já infectados. Como o total de reuniões e amigos são pequenos, podemos fazer um algoritmo com complexidade $$O(N*M)$$. Podemos portanto passar por todas as reuniões, e sempre que houver ao menos uma pessoa infectada, marcamos todos os presentes no vetor de marcação. No final, basta iterar pelo vetor de marcação, e aumentar a resposta por um sempre que houver um infectado marcado.

Complexidade: $$O(N*M)$$

Segue o código, comentado para melhor compreensão: