Seletiva IOI 2018 - Dia 2
Se você quiser se preparar para a OBI e seletiva, não deixe de conferir o Roteiro de estudos de informática, e também a página de informática do NOIC, com todos nossos materiais.
Clique aqui para conferir a prova na íntegra *A seletiva da OBI 2017 foi feita para a IOI 2018
Árvore
Comentário escrito por Enzo Dantas
Subtask 1 (20 pontos):
Essa subtask pode ser resolvida simulando linearmente cada query.
Subimos cada vértice linearmente ("de um em um") até o LCA e inserimos todos os elementos do caminho em um array. Feito isso, ordenamos o array em ordem crescente e retornamos o k-ésimo elemento.
Complexidade:
Cada query é para criar o array e para ordená-lo. Logo, a complexidade final é .
Subtask 2 (40 pontos): e
Conhecimento prévio necessário:
Em problemas com valores pequenos como esse, em que , uma boa intuição é tratar cada valor individividualmente e depois juntá-los de alguma forma.
Nesse caso, imagine que cada vértice tem apenas dois valores possíveis: 0 ou 1. Queremos calcular quantos 1's existem no caminho de a , para depois podermos juntar os 50 valores e descobrir o k-ésimo.
Calculando quantidade de 1's:
É possível adaptar a sparse table do LCA para guardar quantos 1's existem em cada pulo fazendo . Ou seja, assim como na sparse table tradicional do LCA, dividimos o pulo em dois e os juntamos, nesse caso com uma adição.
Entretanto, é possível usar uma ideia mais simples: defina = quantidade de 1's de u até a raiz (o vértice 1 será a raiz). A quantidade de 1's no caminho de a é equivalente a (lembre que estamos assumindo que = 0 ou 1). Calcular o array é bem simples: percorremos a árvore com uma DFS e fazemos .
Esse mesmo processo pode ser repetido 50 vezes (uma vez para cada valor) e teremos o que precisamos - a quantidade de cada valor no caminho de a .
Juntando os valores:
Para cada query, criamos um array auxiliar de tamanho 50 tal que = quantidade de valores no caminho de a . Em seguida, percorremos esse array linearmente até achar o índice do primeiro prefixo que possui uma soma maior ou igual a , e esse índice é a resposta da query.
Complexidade:
Em cada query temos que achar o LCA dos dois vértices e percorrer o array de tamanho 50. Logo, cada query é . Além disso, calcular o array para cada valor é . Assim, a complexidade final fica = .
Subtask 3 (20 pontos):
Conhecimento prévio necessário:
- Euler Tour
- Algoritmo de MO
- Busca em Seg
Aqui, é importante pensar em casos especifícos do problema (, por exemplo, onde teríamos que pegar o mínimo). Um caso muito importante em problemas de caminho em árvore é imaginar que o problema é em um array em vez de em uma árvore. Nesse caso, o problema vira "Ache o k-ésimo menor elemento em intervalos ". Esse problema pode ser resolvido de 2 maneiras: merge sort tree ou MO.
A ideia da merge sort tree não é facilmente aplicável na árvore e possui uma implementação complicada por si só. Além disso, os limites dessa e da próxima subtask sugerem que a solução esperada para essa subtask possui complexidade . Sendo assim, pensamos no algoritmo de MO. A solução possui 3 passos: linearizar a árvore, aplicar o MO, e achar o k-ésimo.
Linearizando a árvore:
Uma ideia conhecida em problemas de caminho em árvore que usam Euler Tour é "cancelar" vértices que estão no meio da ordem da linearização mas não estão no caminho desejado. Por exemplo: imagine que queremos a soma dos vértices da raíz até algum vértice . O jeito de linearizar a árvore para responder tais perguntas é somar o valor quando entramos no vértice e subtrair o valor quando saímos. Assim, o valor de qualquer subárvore no segmento que vai da raíz até será anulado e os valores no caminho que realmente importa estarão disponíveis. Analogamente, podemos inserir o vértice na linearização tanto na entrada como na saída e cancelaremos os duplicados.
MO:
Com a árvore linearizada, o caminho entre vértices e é o intervalo (assumindo que está a esquerda de ). Logo, temos o seguinte problema: retorne o k-ésimo menor elemento não duplicado em vários intervalos de um array. A ideia do MO aqui é análoga à do problema Distinct Values Queries do CSES: mantemos um mark dos elementos que já estão inclusos no intervalo atual e contamos apenas os que aparecem uma única vez. Ou seja, quando encontramos um duplicado, o removemos. Um pequeno detalhe é que podem haver elementos de mesmo valor no problema. Para contornar isso, salvamos um pair (valor, vértice) na linearização e removemos vértices duplicados em vez de valores duplicados.
Achando o k-ésimo:
É possível usar BIT ou Segment Tree aqui, mas usaremos Segment Tree para simplificar a explicação. A x-ésima folha da Seg guardará quantos valores x existem no intervalo atual do MO, então quando adicionamos ou removemos um valor x, fazemos seg.upd(x, ±1). Finalmente, para achar o k-ésimo elemento do intervalo atual do MO, fazemos uma busca na Seg.
Complexidade:
Complexidade do MO: toda vez que movemos um ponteiro fazemos uma operação em na Seg. Logo, como movemos ponteiros vezes em um MO normal, a complexidade desse MO será .
A complexidade da linearização é , então a complexidade final será a complexidade do MO, que é .
Subtask 4 (20 pontos): Nenhuma restrição adicional
Conhecimento prévio necessário:
- Segment Tree persistente
- Busca em Seg
- Lowest Common Ancestor (LCA)
A ideia dessa solução é manter, para cada vértice, uma seg dos elementos no caminho desse vértice até a raiz. Assim, para uma query entre os vértices e com lca , podemos fazer uma busca binária na resposta e fazer uma busca nas Seg's de , , e para verificar se achamos o k-ésimo. Entretanto, cada query teria uma complexidade de , o que fica bem no limite de tempo . Caso seja necessário, com o intuito de otimizar a busca, podemos fazer uma busca nas três segs simultaneamente, o que tem uma complexidade de .
Para manter uma Seg para cada vértice, usamos uma Seg persistente em que cada vértice tem uma versão que aponta para a versão do pai e insere seu valor com um update persistente na Seg.
Complexidade:
Construir cada versão da Seg persistente é , então construir toda a Seg é . Cada query é respondida com uma busca binária, então a complexidade das queries é . Assim, a complexidade final é .