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

Comentário por Rogério Júnior

Para ver o caderno de tarefas da segunda fase da Programação Nível 1 da OBI 2016, clique aqui.

Este comentário utiliza os objetos cin cout como métodos de entrada e saída. Eles estão na biblioteca iostream, do C++, e são de muito fácil uso.

Para ler uma variável qualquer a, basta a linha de código: cin >> a;

Para ler uma sequência de variáveis abc, ... quaisquer, basta a linha de código: cin >> a >> b >> c >> ...;

Para imprimir uma variável qualquer a, basta a linha de código: cout << a;

Para imprimir uma sequência de variáveis abc, ... quaisquer, basta a linha de código: cout << a << b << c << ...;

Medalhas

Conhecimento prévio necessário:

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

Devemos imprimir o número do atleta que terminou a prova em cada lugar. Para tal, em cada posição, basta checarmos o tempo de cada atleta e verificar se ele é quem estamos procurando. No início, vamos ler os valores da entrada e guardar nos inteiros t1t2 t3. Eles representarão o tempo de cada atleta, respectivamente.

Vamos checar se o primeiro atleta foi o que terminou em primeiro lugar: basta checar se o seu tempo é menor que  dos outros dois (if(t1<t2 and t1<t3)). Não precisamos nos preocupar com igualdades pois o enunciado nos diz que os tempos são, necessariamente, diferentes. Caso o primeiro atleta seja de fato o primeiro colocado, imprimimos seu número (cout << "1\n";), e repetimos essa checagem com cada atleta.

Agora, basta repetirmos o mesmo processo para descobrirmos quem são o segundo e o terceiro lugar, mudando, logicamente, apenas a condição. O segundo lugar, por exemplo não tem o tempo menor que o dos outros dois, mas está entre os dois colocados, ou seja, se t1 é o segundo colocado, então ou t2>t1>t3 ou t3>t1>t2 (if((t2>t1 and t1>t3) or (t3>t1 and t1>t2))). Por fim, o último colocado deve ter um tempo maior que os outros dois.

Para cada posição, quando descobrimos qual atleta a ocupa, basta imprimirmos o número dele. Segue o código para melhor entendimento:


// Medalhas - F2PJ - OBI 2016
// Rogério Júnior
// Complexidade: O(1)
#include "iostream"
using namespace std;
int main(){
int t1, t2, t3;
cin >> t1 >> t2 >> t3;
// vou descobrir quem é o primeiro colocado:
if(t1<t2 and t1<t3){ // se for o atleta 1, imprimo 1
cout << "1\n";
}
if(t2<t1 and t2<t3){ // se for o atleta 2, imprimo 2
cout << "2\n";
}
if(t3<t1 and t3<t2){ // se for o atleta 3, imprimo 3
cout << "3\n";
}
// agora, vou descobrir quem é o segundo colocado
// nessa pate, devemos ter muito cuidado em colocar os parêntesis
// pois temos vária condições dentro de um mesmo "if"
// os parêntesis devem separar corretamente as condições
// para que o coputador saiba dar a prioridade desejada ao "and" e ao "or"
if((t2>t1 and t1>t3) or (t3>t1 and t1>t2)){ // se for o atleta 1, imprimo 1
cout << "1\n";
}
if((t1>t2 and t2>t3) or (t3>t2 and t2>t1)){ // se for o atleta 2, imprimo 2
cout << "2\n";
}
if((t1>t3 and t3>t2) or (t2>t3 and t3>t1)){ // se for o atleta 3, imprimo 3
cout << "3\n";
}
// por fim, basta descobrir quem é o terceiro
if(t1>t2 and t1>t3){ // se for o atleta 1, imprimo 1
cout << "1\n";
}
if(t2>t1 and t2>t3){ // se for o atleta 2, imprimo 2
cout << "2\n";
}
if(t3>t1 and t3>t2){ // se for o atleta 3, imprimo 3
cout << "3\n";
}
return 0;
}

view raw

medalhas.cpp

hosted with ❤ by GitHub

Caverna de Ordinskaya

Conhecimento prévio necessário

  1. Estruturas condicionais (Aula 2)

A solução deste problema segue a ideia geral de um algoritmo guloso: basta sempre tentar construir a fita com o menor número e, caso ele seja menor que a medição anterior, uso a maior, pois só há duas possíveis em cada posição. Caso a maior também seja menor que a anterior, é impossível.

De maneira mais formal, seja a_i o i-ésimo número da entrada e suponha que já sabemos que o valor da medição anterior (i-1) é s_{i-1}. Seja x_i o menor entre a_i e M-a_i (o menor valor possível), e y_i o maior entre a_i e M-a_i (o maior valor possível). Em outras palavras,  x_i=max(a_i,M-a_i) e y_i=min(a_i,M-a_i). Se y \geq s_{i-1}, devemos utilizá-lo, ou seja determinar que s_i=y_i, pois é o melhor que podemos fazer para minimizar o comprimento da corda. Se isto não for possível, então só nos resta utilizar o valor de x_i (s_i=x_i). Note que, se x_i<s_{i-1}, então é impossível atribuir algum valor a s_i, pois s_{i-1} foi construído de modo a ter o menor valor possível.

Assim, basta guardarmos a variável resp e a variável ant. No começo, resp=ant=0. Depois, usamos um for para percorrermos todas as medições. Em cada iteração iant será o valor de s_{i-1}, e resp  o atual comprimento da corda. Se for possível atribuir algum valor a s_i, somamos seu tamanho a resp. Caso contrário, resp=-1 e paro o loop. Não podemos esquecer de atualizar o valor de ant para s_i, para a próxima iteração do loop.

Por fim, basta imprimir o valor salvo em resp. Note que ant começa 0 para que seja sempre possível usar o menor valor na primeira medição. Como a soma total pode ser, num limite bem rude, N \times M = 5 \times 10^9, usaremos long long. Segue o código para melhor entendimento:


// Caverna de Ordinskaya - F2PJ - OBI 2016
// Rogério Júnior
// Complexidade: O(N)
#include "iostream"
#include "algorithm" // min e max
using namespace std;
int main(){
// declaro as variávei que vou usar
long long n, m, resp=0, ant=0;
// leio os valores de N e M
cin >> n >> m;
// para cada medição feita
for(int i=0;i<n;i++){
// leio o valor medido
long long a;
cin >> a;
// x é o maior valor possível e y o menor
long long x=max(a,m-a), y=min(a,m-a);
// se possível, uso y
if(y>=ant){
resp+=y; // adiciono y ao comprimento total
ant=y; // e atualizo o valor de ant
}
// se não, uso x
if(y<ant and x>=ant){
resp+=x; // adiciono x ao comprimento total
ant=x; // e atualizo o valor de ant
}
// se nem x for possível, é impossível
if(x<ant){
resp=-1; // a respsota será -1
break; // e dou um break no loop
}
}
// por fim, imprimo a resposta
cout << resp << "\n";
// e retorno zero
return 0;
}

view raw

caverna.cpp

hosted with ❤ by GitHub

Arco e Flecha

Conhecimento prévio necessário

  1. Número de inversões no vetor (Aula 5)

Seja d_i a distância ao quadrado da flecha i até o centro. Note que se d_i  data-recalc-dims= d_j" />, então a flecha i está mais distante do centro do alvo que a flecha j. Usaremos os quadrados das distâncias para não termos que trabalhar com double, o que poderia acarretar em perda de precisão. Seja S={d_i, 1 \leq i \leq N}, ou seja, S é a sequência formada pelas N distâncias.

Observe agora o tiro i. Olhando para todos os tiros j anteriores a ele (j<i), todos os que tiverem distância menor que ele do centro constituem uma penalidade (se d_j \leq d_i, há uma penalidade), ou seja, os tiros que não irão acrescentar penalidades são todos aqueles anteriores a j que são mais distantes do centro que ele (d_j data-recalc-dims=d_i" />). Deste modo, cada tiro anterior a i que não resultará em uma penalidade no tiro i são aqueles cujo par (j,i) é uma inversão na sequência S (j<i, d_j data-recalc-dims=d_i" />).

Assim, qualquer par de tiros i,j, com i<j será uma penalidade no momento em que o tiro j for dado, a não ser que este par seja uma inversão na sequência. O número total de pares (i,j), i \neq j é \binom{N}{2}= \frac{N \times (N-1)}{2}, do qual seja I o número total de inversões em S. A penalidade total será exatamente \binom{N}{2} - I.

Para resolvermos o problema, basta usarmos um vetor para representarmos a sequência S. Lembre-se que o quadrado da distância de um ponto à origem do plano é, simplesmente, a soma dos quadrados de suas coordenadas. Ou seja, se as coordenadas do ponto i são X_i e Y_i, então v[i] = d_i = X_i^2+Y_i^2. Como as coordenadas têm módulo que pode ir até 10^6, usaremos long long para guardarmos os valores do vetor v.

Por fim, basta calcularmos o número de inversões em v, de maneira exatamente igual ao que foi feito no fim da aula de ordenação, e imprimirmos a resposta. Segue o código para melhor entendimento:


// Arco e Flecha - F2P1 - OBI 2016
// Rogério Júnior
// Complexidade: O(N log N)
#include "iostream"
#include "algorithm"
using namespace std;
#define MAXN 100100
// defino o tipo ll como long long
typedef long long ll;
// vetores da função merge_sort
ll aux[MAXN], v[MAXN];
ll merge_sort(int ini, int fim){ // declaro a função merge_sort, que agora retorna um ll
if(ini==fim) return 0; // se o intervalo tiver um único elemento, ele não tem inversões
// caso o contrário, declaro a variável invers, que começa com a soma das inversões das duas metades
ll invers=merge_sort(ini, (ini+fim)/2) + merge_sort((ini+fim)/2+1, fim); // observe que chamei a recursão e ordenei as metades
int tam=0, j=(ini+fim)/2+1; // declaro tam e j, como feito no código anterior do merge_sort
for(int i=ini; i<=(ini+fim)/2; i++){ // para cada posição da metade da esquerda
while(j<=fim && v[j]<v[i]){ // procuro os elementos da metade da direita menores que i
// os adiciono ao vetor aux
aux[tam]=v[j];
tam++;
j++; // passo para o próximo elemento
invers+=(ini+fim)/2-i+1; // e adicino o número de inversões em metades diferentes com o elemento j
}
// adiciono o elemento i
aux[tam]=v[i];
tam++;
}
// adiciono o resto dos elementosda segunda metade
while(j<=fim){
aux[tam]=v[j];
tam++;
j++;
}
for(int i=ini; i<=fim; i++) v[i]=aux[i-ini]; // e troco os valores do vetor original pelos ordenados
return invers; // retorno o número de inversões calculado
}
int main(){
// declaro e leio o valor de n
ll n;
cin >> n;
// para cada flecha
for(int i=1;i<=n;i++){
// leio suas coordenadas
ll x, y;
cin >> x >> y;
// e guardo, em v, o quadrado de sua distância ao centro
v[i]=x*x+y*y;
}
// por fim, imprimo a resposta,
// o número de pares em v menos o número de inversões
cout << (n*(n-1)/2) - merge_sort(1,n) << "\n";
// e retorno zero
return 0;
}

view raw

arco.cpp

hosted with ❤ by GitHub

Caminhos do reino

Conhecimento prévio necessário

  1. Vetores (Aula 3)
  2. Grafos (Aula 8)

Este problema envolve uma implementação minuciosa, dividida em várias etapas. Seja d(v,u) o tamanho do caminho que sai de v e chega em u, se existir. Para cada vértice v do grafo, devemos calcular os valores de pos_v e h_v. Seja c_v a cidade do ciclo interno tal que d(v,c_v) é mínimo. Se v está no ciclo, c_v=v. Defina h_v como d(v,c_v).

O ciclo tem um pai, que é um vértice u qualquer pertencente ao ciclo. Se k é um vértice qualquer do ciclo, defina pos_k como d(u,k). Note que pos_u=0. Agora, para qualquer vértice v, defina pos_v=pos_{c_v}.

Agora que já definimos esses valores, calcular o menor tempo de encontro entre duas pessoas nos vértices v e u do grafo é bem simples. Se ambos os vértices estiverem no mesmo caminho periférico, ou seja, c_v=c_u, a distância entre eles é \mid h_v-h_u \mid, pois basta que a pessoa na cidade de maior "profundidade" caminhe até chegar na cidade mais próxima do ciclo.

Entretanto, se os vértices estiverem em caminhos periféricos diferentes, a conta é ligeiramente mais complicada. Primeiramente, note que os dois vão, necessariamente, se encontrar em algum vértice do ciclo. Quando um dos dois chega no ciclo, não vale a pena que os dois se movam nele, pois qualquer movimento de um distancia-se do outro, visto que só podem andar em uma direção, logo um dos dois, digamos v, não se movimentará no ciclo e o ideal será que se encontrem no vértice c_v.

De maneira mais formal, suponha que o vértice ideal de encontro é k diferente de c_v e c_u. Sem perda de generalidade, digamos que d(c_v,k)<d(c_u,k). Neste caso, como o ciclo só tem um sentido, o caminho do c_u a k necessariamente passa pelo vértice c_v, sendo melhor que ambos se encontrassem, portanto, em c_v.

Sabendo agora que o ponto de encontro será c_v ou c_u, basta calcularmos qual dos pontos de encontro é a resposta. Seja d_v o tempo em que as pessoas no vértice v e u se encontram em c_v. Note que d_v=max(d(v,c_v),d(u,c_v)), pois veremos quanto tempo cada um demora para chegar em c_v e a resposta será o maior deles (o que chega mais rápido espera o outro). Sabemos que d(v,c_v)=h_v, restando-nos calcular o valor de d(u,c_v). Para chegar de u a c_v, primeiro temos que chegar ao ciclo, ou seja chegar em c_u, e sabemos que d(u,c_u)=h_u. Feito isso, basta calcularmos o valor de d(c_u,c_v), pois sabemos que d(u,c_v)=d(u,c_u)+d(c_u,c_v). Se pos_{c_v} data-recalc-dims=pos_{c_u}" />, então d(c_u,c_v)=pos_{c_v}-pos_{c_u}. No entanto, se pos_{c_v}<pos_{c_u}, d(c_u,c_v)=(pos_{c_v}-pos_{c_u}) \mod m, onde m é o tamanho do ciclo interno. De qualquer modo, d(c_u,c_v)=(pos_{c_v}-pos_{c_u}) \mod m. Assim,d_v=max(h_v,h_u+((pos_{c_v}-pos_{c_u}) \mod m). De maneira análoga, d_u=max(h_u,h_v+((pos_{c_u}-pos_{c_v}) \mod m).

Deste modo, o menor tempo para que pessoas nos vértices u e v se encontrem é min(d_v,d_u). Note que parto do princípio de que a \mod b sempre é positivo, ou seja, se a data-recalc-dims=0" />, -a \mod b = b-a. Agora, basta implementarmos um código que calcule os valores de h_v e pos_v para todo vértice v do grafo.

Primeiramente, vamos identificar todos os vértices que estão no ciclo e guardá-los no vector cycle. Para isso, vamos salvar as arestas em dois grafos: um original, em que son[v] guarda o destino da aresta que sai de v, e um grafo inverso, em que o  vector invers[v] guarda todos os vértices cujas arestas chegam em v.

Agora, para encontrar um cara no ciclo, basta percorrer todos os vértices, procurando por um com grau de entrada maior que 1, ou seja invers[v].size()>1. Quando o encontramos, basta seguirmos o caminho que começa nele no grafo normal, adicionando todos os vértices que passaremos ao ciclo, até que voltemos a v, e então paramos.

Agora, para cada vértice do ciclo, vamos chamar uma DFS no grafo inverso, que irá calcular os valores de h[v]pos[v] para todos os vértices do grafo. Para o vértice v no ciclo, h[v] já começa 0pos[v] será sua posição em cycle.

Agora, suponha que chamamos a DFS para um vértice v qualquer. Para cada vizinho seu u, se ele não estiver no ciclo, o valor de pos[u] será o mesmo de pos[v], pois eles chegam ao mesmo vértice do ciclo, e como o caminho de u a esse vértice consiste em ir de até e então ao vértice, temos que h[u]=h[v]+1. Agora que já calculamos esses valores para u, podemos chamar dfs(u).

Depois de chamada a DFS para todos os vértices do ciclo, já teremos os valores de hpos para qualquer vertice do grafo, logo, basta ler cada uma das perguntas e respondê-las com as fórmulas calculadas anteriormente. Segue o código para melhor entendimento:


// Caminhos do Reino - F2P1 - OBI 2016
// Rogério Júnior
// Complexidade: O(N+Q)
#include "iostream"
#include "algorithm"
#include "vector"
using namespace std;
#define PB push_back
#define MAXN 100100
int n, q, v, son[MAXN], pos[MAXN], h[MAXN];
bool in_cyc[MAXN];
vector<int> invers[MAXN], cycle;
// função para calcular a distância no ciclo
int mod(int x){
// se x for negativo
// tenho q dar a volta pelo "outro lado"
if(x<0) x+=cycle.size();
// e retorno x
return x;
}
// DFS que calcula o "pai" do vértice no ciclo
// e sua distância dele
void dfs(int v){
// para cada filho de v no grafo inverso
for(int i=0;i<invers[v].size();i++){
int u=invers[v][i];
// se ele já estiver no ciclo, ignoro
if(in_cyc[u]) continue;
// se não, ele recebe a posição de v
pos[u]=pos[v];
// e sua profundidade mais um
h[u]=h[v]+1;
// e chamo a DFS nele
dfs(u);
}
}
int main(){
// leio o valor de N
cin >> n;
// leio cada aresta
for(int v=1;v<=n;v++){
int u;
cin >> u;
// a salvo no grafo direto
son[v]=u;
// e no grafo inverso
invers[u].PB(v);
}
// percorro os vértices procurando algum no ciclo
for(int v=1;v<=n;v++){
// se ele tem grau maior que um no cico inverso
// há dois caminhos que chegam nele, logo ele está no ciclo
if(invers[v].size()>1){
// o adiciono ao ciclo
cycle.PB(v);
in_cyc[v]=true;
// e sigo seu caminho de filhos até completar todo o ciclo
int u=son[v];
while(u!=v){
// marco que o cada vértice está no ciclo
in_cyc[u]=true;
// o adiciono no vetor cycle
cycle.PB(u);
// e olho o seu filho
u=son[u];
}
// quando encontro o ciclo, posso parar o for
break;
}
}
// para cada vértice no ciclo
for(int i=0;i<cycle.size();i++){
int v=cycle[i];
// salvo sua posição no ciclo
pos[v]=i;
// e salvo a altura e posição no ciclo
// de cada vértice do caminho que sai dele
dfs(v);
}
// leio o valor de Q
cin >> q;
for(int i=0;i<q;i++){
// leio os vértices da pergunta
int a, b;
cin >> a >> b;
// se os dois estão no mesmo caminho periférico
// basta imprimir a diferença entre suas alturas
if(pos[a]==pos[b]) cout << max(h[a]-h[b],h[b]-h[a]) <<" \n";
// se não
else{
// calculo o tempo para que se encontrem no pai de A e de B
int da, db;
da=max(h[a],h[b]+mod(pos[a]-pos[b]));
db=max(h[b],h[a]+mod(pos[b]-pos[a]));
// e imprimo o menor deles
cout << min(da,db) << "\n";
}
}
// por fim, retorno zero
return 0;
}

view raw

caminhos.cpp

hosted with ❤ by GitHub