OBI 2021 - Fase 3 - Programação Nível 1
Para se preparar para a OBI, confira o nosso Roteiro de Estudos, nele você vai encontrar um direcionamento completo de como estudar!
Comentário escrito por Pedro Racchetti, Lúcio Figueiredo e Leonardo Paes
Para conferir a prova na íntegra, clique aqui.
Casamento de inteiros
Conhecimento prévio necessário:
Inicialmente, leremos e como strings e ajustaremos seus tamanhos adicionando zeros à esquerda até que o tamanho de seja igual ao tamanho de . Em seguida, vamos delcarar duas strings e que guardarão os valores e após o casamento. Após isso, iteraremos por cada dígito (ou caractere) de e e iremos compará-los assim como indicado no enunciado do problema, adicionando os dígitos resultantes do casamento em ou . Por fim, precisamos remover quaisquer zeros à esquerda restantes em e ; para isso, usaremos a função , que transforma strings em inteiros e remove zeros à esquerda.
A complexidade da solução é . Confira o código abaixo para melhor compreensão:
Sr. Sapo
Conhecimento prévio necessário:
Para resolvemos esse problema, podemos visualizar a matriz dada como um grafo. Cada célula da matriz será um vértice do grafo. Haverá uma aresta entre dois vértices se, e somente se, as três condições a seguir forem verdadeiras:
- Ambos os vértices são pedras;
- O pulo que o Sapo dá para ir de uma pedra para a outra é paralelo a um dos lados do lago;
- A distância do pulo é menor ou igual a .
Segue uma imagem mostando todas as arestas partindo de um vértice arbitrário , supondo que todos os vértices sejam pedras:
Após criado tal grafo, basta utilizar algum algoritmo de busca em grafos, como por exemplo uma DFS ou uma BFS. Iniciamos o algoritmo na posicão inicial do Sapo. Se em qualquer momento visitarmos a posição final, sabemos que é possível para o Sapo chegar lá; caso contrário, é impossível!
A complexidade da solução é . Confia o código abaixo para melhor compreensão:
Festa olímpica
Conhecimento prévio necessário:
- Vector da STL
- Segment Tree e Busca Binária na Segment Tree (Utilizadas na parcial 2)
Esse problema apresentou 4 pontuações parciais, que serão discutidas separadamente.
Parcial 1 ( e )
Para essa parcial, podemos inicializar um com todas as posições iniciais, e remover as posições apropriadas do , usando a função . Note que precisamos pré-computar os itens a serem removidos, para se manter a ordem das posições.
A função em um , ao contrário de um , por exemplo, tem complexidade , e como, independente de , cada elemento inicial será removido no máximo uma vez, essa solução tem complexidade . Segue o código, para melhor compreensão.
Parcial 2 ( e )
Para essa parcial, podemos adaptar a solução anterior, porém ao invés de se manter um com os elementos ainda ativos, podemos manter uma Segment Tree, que irá manter, para cada posição da fila inicial, 1 se a posição estiver ativa, e 0 se não. Dessa forma, a soma de prefixo de uma certa posição ativa indica o número da posição na fila após alguns passos.
Numa árvore de segmentos, podemos "remover" uma posição, ou seja, transformar seu valor em 0, com complexidade de , e encontrar qual é o i-ésimo valor ativo também em , usando Busca Binária, adaptada para a árvore de segmentos. Dessa forma, como um número pode apenas ser removido da fila uma vez, temos uma complexidade total de . Segue o código, para melhor compreensão.
Parcial 3 (, para todo )
Observação: essa parcial pode ser considerada mais fácil que a anterior, mostrando a importância de sempre ler todas as parciais de um problema, quando não se sabe resolvê-lo inteiro, já que as parciais não estão necessariamente em ordem crescente de dificuldade.
Para essa parcial, é útil se observar padrões. Observe o caso de e
- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 <- Inicial.
- 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25 <- Após o primeiro passo. Sobraram apenas números da forma .
- 1, 5, 9, 13, 17, 21, 25 <- Após o segundo passo. Sobraram apenas números da forma .
- 1, 9, 17, 25 <- Após o terceiro passo. Sobraram apenas números da forma , ou seja .
Nota-se então, que após o k-ésimo passo, sobram apenas os números de forma . Isso não é uma demonstração formal, porém essa é relativamente fácil de se encontrar, por meios de indução. Note também que, para , , então, após passos, não haverão mais números possíveis, exceto o .
Essa solução tem complexidade , porém com , temos complexidade . Segue então o código, para melhor compreensão.
Parcial 4 (Sem restrições adicionais)
Agora, chegamos a cereja do bolo: o problema completo!
Vamos mais uma vez analisar um caso, dessa vez, , , e .
- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
- 1, 3, 5, 7, 9, 11, 13, 15, 17, 19
- 1, 3, 7, 9, 13, 15, 19
Dessa vez, o padrão não é tão evidente. Observe porém, se olharmos apenas as posições da fila de cada passo, não seu valor original, e X para as posições removidas.
- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
- 1, X, 2, X, 3, X, 4, X, 5, X, 6, X, 7, X, 8, X, 9, X, 10, X
- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 <- Após as remoções do primeiro passo.
- 1, 2, X, 3, 4, X, 5, 6, X, 7
- 1, 2, 3, 4, 5, 6, 7 <- Após as remoções do segundo passo.
Não é óbvio o que acontece com uma posição quando progredimos de um passo para o próximo. Porém, observe o que acontece com uma posição quando regredimos de um passo para o anterior: o valor no passo anterior da posição é somente a posição, somada ao número de X antes dela! Ou seja, como sabemos que os números finais são limitados, conseguimos fazer as operações em reverso. Basta agora saber quantos X haverão antes de uma posição qualquer em um passo. Para isso, perceba que, entre o i-ésimo e o i+1-ésimo passo haverá um X a cada números. Porém, após o i-ésimo passo, todos os X que estavam sendo contados são removidos, então, para regredir, se adiciona um X a cada números para todos os anteriores a um índice específico. Logo, serão adicionados números anteriores a ao se regredir de um passo para o anterior.
Dessa forma, conseguimos calcular qual seria a posição inicial de cada posição final após todos os passos. Essa solução tem complexidade , onde S é o tamanho do conjunto dos números da saída. Segue o código, para melhor compreensão.
Plano de estacionamento
Conhecimento prévio necessário:
Para resolver este problema, usaremos a seguinte estratégia gulosa:
- Para o -ésimo cliente, encontre a maior vaga não-ocupada menor ou igual a ;
- Caso não exista nenhuma vaga não-ocupada menor ou igual a , termine o programa;
- Caso contrário, marque a vaga encontrada como ocupada e repita o algoritmo para o cliente seguinte.
A intuição por trás do funcionamento desta estratégia é que, para cada cliente, sempre selecionaremos a sua "pior" vaga possível. Em outras palavras, ao escolhermos a maior vaga possível para um cliente, tornamos o conjunto de vagas não-ocupadas melhor possível para clientes futuros (já que uma vaga de menor índice acomoda todos os clientes que uma vaga maior também acomodaria).
Para implementar o algoritmo acima, basta utilizarmos um set: Inicialmente, inserimos no set todas as vagas; em seguida, podemos encontrar a maior vaga menor ou igual a utilizando a função upper_bound implementada no set (mais detalhes no código abaixo); por fim, removemos a vaga encontrada (se existir) utilizando a função erase.
Como as operações realizadas no set possuem complexidade , a complexidade da solução é . Confira o código abaixo para melhor compreensão: