Seletiva IOI 2020 - 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 2019 foi feita para a IOI 2020
Saldo
Comentário escrito por Estela Baron
Subtask 1: K = 0
Para resolver esta tarefa, basta verificar se a soma total dos saldos é igual a 0.
Subtask 2: N <= 16
Conhecimento prévio:
Vamos estabelecer o vértice 1 como a raiz da árvore. Com a técnica de representar um conjunto em um número (bitmask), podemos gerar todos os subconjuntos que representam as estradas que foram retiradas. Para cada conjunto, basta verificar se tem k estradas retiradas e se a soma de cada componente é igual a zero - para verificar, podemos chamar uma dfs que vá percorrendo toda a árvore, mas que separe os valores quando passar por uma estrada retirada. Se as condições anteriores forem verificadas, precisamos somar os custos e ver se é menor que o custo anterior - caso exista. Assim, obtemos a nossa resposta em , que passa para o limite da subtask.
Subtask 3: N <=100 e |Di| <= 1000 e estrada i conecta as cidades i e i+1
Conhecimento prévio:
|Di| neste caso - enunciado do codeforces- deve ser |Ri|, ou seja, o saldo da cidade i. Nesta subtask, temos uma linha. Então, podemos pensar em fazer algum tipo de dp para calcular a resposta. Observe ainda que a condição de N<=100 nos possibilita inferir que K < 100, pois K < N. Vamos construir os nossos estados da seguinte maneira: dp[x : vertice_i][y: quantidade de estradas dos vértices anteriores destruídas] = menor custo possível para obter grupos com soma igual a zero com y estradas destruídas até o vértice x. Para construir esta dp, vamos imaginar que a nossa linha acaba no vértice x. Depois disso, vamos percorrer todas as posições dos vértices 1 até x-1 e ver qual valor obtemos ao destruir a estrada que liga aquele vértice_w ao seguinte. Para fazer o teste, verificamos por meio de uma soma de intervalos se a soma dos saldos dos vértices de w+1 até x é igual a zero. Caso seja zero, verificamos se dp[w][y-1] é um valor diferente de -1, significando que existe tal configuração, e somamos o custo da estrada que liga o vértice_w ao vértice_w+1. Por fim, caso seja o primeiro valor diferente de -1, vamos colocar que dp[x][y] = dp[w][y-1] + C_w. Caso contrário, atualizamos o valor para o mínimo: dp[x][y] = min(dp[x][y], dp[w][y-1] + C_w).
O caso inicial de cada vértice é dp[x][0] = 0 se a soma dos saldos de 1 até x for 0 ou dp[x][0] = -1 se a soma dos saldos de 1 até x for diferente de 0. A complexidade final fica O(N^3), pois temos N vértices para percorrer, no máximo N-1 para K e, para cada estado, percorremos o vetor para achar o menor valor de dp[w][y-1].
Subtask 4: N<=5000
Nesta tarefa, vamos adaptar a ideia da subtask anterior: vamos tentar achar uma forma de construir a dp[x][y] sem ter que percorrer todos os outros estados. Observe que, se a soma dos saldos da posição 1 até a posição x for diferente de zero, então não tem como encontrar uma configuração válida. No entanto, se for 0, podemos encontrar o menor valor de dp[w][y-1], tal que w seja algum vértice menor que x. Assim, sabemos que a soma acumulada até w é zero e podemos garantir que a soma de w+1 até x também é igual a zero. Com isso, basta fazer um for iterando pelo y e, dentro desse loop, um for para iterar pelas posições, lembrando de guardar o valor do menor dp[][] até x com y-1 estradas - e ir atualizando para as outras posições à medida que os outros valores vão sendo calculados.
A complexidade final fica .
Subtask 6: Sem restrições adicionais - solução final
Conhecimento prévio necessário:
Vamos pensar em uma árvore e suas sub-árvores. Se a soma dos saldos totais da árvore for diferente de 0, nem precisamos calcular mais nada, pois é só imprimir -1. Agora vamos supor que só queremos retirar uma estrada: em uma árvore cuja a soma total é zero, se acharmos uma sub-árvore que tem soma total também igual a zero, podemos destruir a estrada que conecta a árvore principal com a sua sub. De fato, esta observação também se aplica à sub-árvore e às suas sub-árvores, pois a soma total já é 0 - então, se acharmos uma sub com soma 0, o restante só pode ter soma igual a 0. Portanto, percorremos o grafo com uma dfs e vamos salvando qual é a soma dos saldos da sub-árvore do vértice em questão. Quando acharmos uma sub-árvore com soma igual a zero, guardamos o valor da aresta que liga ela com o restante do grafo. Ao final, teremos uma lista de possíveis estradas/arestas que podem ser destruídas. Como queremos exatamente K estradas e com o menor custo possível, temos que ordenar de maneira crescente, ver se existem pelo menos K arestas e somar as K primeiras.
Com isso, obtemos a nossa resposta com uma complexidade de O(N).
Clique aqui para conferir o código.
Carros
Comentário por Murilo Maeda
Conhecimento prévio necessário
Observação 0
O problema não tem nada a ver com carros.
Observação 1
Mover um doce pode ser interpretado como se estivéssemos o removendo da esteira, já que, se estamos na esteira , podemos só movê-lo o quanto for necessário para a direita e na esteira podemos movê-lo quanto for preciso para a esquerda.
Observação 2
Não faz sentido remover um doce da esteira se você não for remover os outros doces mais à esquerda e não faz sentido remover um doce da esteira se você não for remover os outros doces mais à direita. Isso porque se, a partir de um certo momento mais nenhum doce for retirado, o tempo até a condição imposta pelo enunciado ser satisfeita depende apenas do doce mais à esquerda da esteira e do doce mais à direita da esteira . Então, as operações gastas movendo doces que estão "no meio" da esteira poderiam ter sido usadas para retirar os doces que definem o tempo até o final, consequentemente reduzindo o tempo total.
Observação 3
A ordem dos doces retirados não afeta o tempo final.
Observação 4
Se a condição ainda não foi satisfeita, não faz sentido não remover um doce, já que mesmo que a condição seja cumprida nos próximos segundo com o movimento das esteiras, remover um docepossui pelo menos a mesma velocidade do movimento das esteiras. Então, podemos considerar que a cada segundo será removido um doce, ou que, o número de doces removidos vai ser igual ao tempo total.
Solução
Com essas observações, podemos estabelecer que os doces retirados da esteira formarão um prefixo dos doces iniciais e os doces retirados da esteira formarão um sufixo dos doces iniciais. A partir disso, queremos testar as configurações possíveis a fim de achar o tempo final mínimo.
Para fazer isso de forma eficiente, vamos fixar um determinado prefixo de doces retirados da esteira e ver até qual sufixo da esteira precisamos retirar de modo que a condição do enunciado seja satisfeita. De forma mais específica, queremos, dado um doce na esteira na posição , de modo que seja o doce mais à esquerda dessa esteira (ou ele é inicialmente o mais à esquerda ou os outros foram retirados) queremos saber até qual elemento da esteira é preciso remover de modo que seja maior ou igual do que o elemento mais a direita na esteira .
Como lidar com as esteiras se movendo
Como é preciso levar em conta o tempo para retirar os doces e como temos a observação 4, vamos fazer isso da seguinte forma: passamos pelos elementos da esteira e em cada um deles, vamos considerar que todos os doces antes dele foram retirados e adicionar esse tempo no atual (para simular que a esteira se moveu). Mais especificamente, o que será usado para comparar será , já que a esteira se move para a direita e foram usados segundos para remover os elementos à esquerda de . Como esse doce será o mais à esquerda na esteira , podemos usar só ele para comparar com os doces da esteira . Na esteira vamos fazer algo semelhante, vamos criar um vetor que vai manter em cada índice qual seria a posição do doce inicialmente em considerando que todos os doces para sua direita foram removidos. Mais especificamente, , já que a esteira se move para a esquerda e foram gastos segundos removendo os doces para a direita. Perceba que o tempo gasto para retirar os elementos na esteira também afeta os elementos da esteira e vice-versa. Então, precisamos contabilizar isso na comparação. Basicamente, queremos saber se . Em outras palavras, queremos saber se quando retiramos os doces com índice no intervalo de e todos os doces com índice no intervalo de , o doce mais à esquerda de é maior ou igual ao doce mais a direita de . Se isso for verdade é porque o ciclo acaba e o tempo gasto seria .
Então, poderíamos passar por todos os índices tais que e para cada um deles passar pelos índices tais que ,do maior para o menor e, no primeiro índice que a condição for verdadeira, paramos, já que considerando que da esteira só tiramos os doces antes de , tirar os elementos depois de em é o mínimo possível para que o ciclo acabe.
O código ficaria assim
Note, no entanto, que o limite do problema é com e nossa solução é , então temos que otimizar a complexidade para passar para 100 pontos (ignore o fato de que os casos estão ruins e essa solução passa mesmo sendo quadrática).
Otimização
Perceba que, como o vetor é crescente e estamos procurando o primeiro valor de modo que , podemos fazer uma busca binária para procurar pelo índice que satisfaz essa condição. Mas, o problema é que na inequação atual, a parte do vetor depende do índice e a parte de depende do item , ou seja, não é possível fazer a busca binária, já que um termo depende do outro.
Então, iremos mudar a inequação de modo que o lado do possa ser independente do índice do atual em (para que possa ser pré-computado) e de modo que o lado do não dependa do índice do . Podemos modificar a inequação para ela ficar assim: e agora, cada um dos lados é independente. Podemos colocar o dentro da definição de , que agora fica . Agora, podemos pegar o índice do upper_bound de no vetor e subtrair desse índice para obter o primeiro índice (do maior para o menor) que satisfaz nossa inequação.
Então, o código fica assim.
Palavras
Comentário por Henrique Vianna e Fernando Gonçalves
Conhecimento prévio necessário:
Na resolução desse problema, exploraremos as soluções das parciais mais importantes:
Parciais 1 e 2: , , apenas os números de a são usados.
Nessas duas parciais, basta checarmos se a string é substring de , de tamanho . Isso é verdade pois não podemos fazer nenhuma alteração nela () e o numero máximo que pode aparecer em nossa string é . Uma maneira simples de fazer isso é gerar , adicionar todas suas substrings num map/set e depois apenas verificar se a string atual se encontra nessa estrutura.
Parciais 3, 4, 5 e 6: , , de memória.
Pense em todos os índices em que o ocorre. Perceba que esses devem estar todos em posições pares, ou todos em posições ímpares (por conta da forma que a string foi construída) . Imagine agora que fixamos a paridade dos elementos , ou seja, escolhemos se eles estarão todos em posições pares ou todos em posições impares. Ao removermos esses elementos da string, nos deparamos com um problema análogo, em que o possui propriedades semelhantes ao . A principal observação para esse problema é justamente essa: pensar de forma recursiva.
Para melhor simular essas escolhas, utilizaremos uma árvore binária. A raiz "conterá" nossa string inteira. A aresta para seu filho esquerdo representará a escolha de que o ocorre nos índices pares, enquanto a aresta para o filho direito fará o mesmo mas considerando que o ocorre em posições impares. Colocaremos como o peso dessas arestas o numero de alterações na string que tivemos que fazer (elementos em posições pares diferentes de , para o filho esquerdo da raiz, por exemplo). Nos filhos da raiz em si consideraremos as novas strings, com os índices da paridade do elemento já removidos. Um raciocínio parecido será aplicado aos demais nós. Perceba que o que estamos fazendo é análogo a escolher um resto na divisão por potências de para cada número. Assim, só precisamos fazer isso para os números menores ou iguais a , já que (não podemos ter dois números maiores ou iguais a numa mesma sequência).
Tendo essa árvore, nosso problema pode ser reduzido a achar o caminho de menor custo da raiz a uma das folhas da árvore, para depois compararmos esse valor com o de . Para tanto, podemos manter para cada nó o minimo custo para, a partir dele, chegar a alguma folha (de forma similar ao que fazemos numa segment tree de minimo). Não precisamos, de fato, guardar as strings em cada nó. Além disso, perceba que num update teremos de atualizar apenas um caminho da raiz a uma folha, já que o índice em questão estará sempre contido em exatamente um dos dois filhos. Assim, é fácil fazermos updates em .
Veja o código abaixo:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Parciais 1-6: 84 pontos | |
#include <bits/stdc++.h> | |
using namespace std; | |
const int MAXN = (1 << 21) + 10; | |
array<int, 2*MAXN+1> seg, peso; | |
void refresh(int pos, int nivel) // Atualiza o no pos | |
{ | |
if (nivel == 21) {seg[pos] = peso[pos]; return;} | |
int e = 2*pos, d = e+1; | |
seg[pos] = peso[pos] + min(seg[e], seg[d]); | |
} | |
void update(int pos, int nivel, int x, int id, int val) //pos na seg, nivel na arvore, nivel que quero chegar, id na seq, val pra atualizar | |
{ | |
if (nivel == x) return; | |
vector<int> viz = {2*pos, 2*pos + 1}; | |
int chosenViz = 0; | |
if (nivel+1 == x) chosenViz = (id&1); | |
else chosenViz = !(id&1); | |
peso[viz[!chosenViz]] += val; | |
update(viz[chosenViz], nivel+1, x, id>>1, val); | |
refresh(viz[!chosenViz], nivel+1); | |
refresh(pos, nivel); | |
} | |
int main() | |
{ | |
ios_base::sync_with_stdio(false); | |
cin.tie(NULL); | |
int N, Q, K; | |
cin >> N >> Q >> K; | |
vector<int> seq(N); | |
for (auto &x : seq) cin >> x; | |
seq.insert(seq.begin(), 0); | |
for (int i = 1; i <= N; i++) | |
{ | |
if (seq[i] > 21) seq[i] = 21; | |
update(1, 0, seq[i], i, +1); | |
} | |
cout << (seg[1] <= K ? 1 : 0) << '\n'; | |
while (Q--) | |
{ | |
int i, X; | |
cin >> i >> X; | |
update(1, 0, seq[i], i, -1); | |
seq[i] = min(X, 21); | |
update(1, 0, seq[i], i, +1); | |
cout << (seg[1] <= K ? 1 : 0) << '\n'; | |
} | |
} |
Parciais 7: , , de memória.
Para resolver a ultima parcial desse problema, precisamos otimizar a memória que estamos utilizando. Ainda não utilizamos o fato de que o valor de é pequeno. Exploraremos esse fato para reduzirmos nosso uso de memória. Perceba que, na maioria dos casos, não precisamos de fato criar os dois filhos para cada nó, já que frequentemente o custo já seria maior do que o de . Manteremos dinamicamente uma quantidade reduzida de nós, apagando els quando possível e mantendo apenas os que são realmente necessários no momento. Perceba que isso significa que teremos de reconstruir a nossa árvore depois de certos updates, fato que deve ser considerado na implementação dessa solução (veja o willbuid, no código abaixo).
Há vários casos e detalhes que devem ser tratados durante a implementação. Veja o código abaixo caso precise de ajuda:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <bits/stdc++.h> | |
using namespace std; | |
int N, Q, K; | |
// Informacoes dos nos | |
vector<int> seg, peso; | |
vector<int> e, d, pai; | |
vector<bool> developed; | |
vector<int> seq; | |
set<int> avail; // Guarda nos que posso usar | |
int create() // Retorna no que posso usar (ou cria um se nao tiver) | |
{ | |
if (avail.empty()) | |
{ | |
seg.push_back(0); | |
peso.push_back(0); | |
e.push_back(0); | |
d.push_back(0); | |
developed.push_back(0); | |
pai.push_back(0); | |
avail.insert((int)seg.size()-1); | |
} | |
int pos = *avail.begin(); avail.erase(avail.begin()); | |
seg[pos] = 0; | |
peso[pos] = 0; | |
e[pos] = 0; | |
d[pos] = 0; | |
developed[pos] = 0; | |
pai[pos] = 0; | |
return pos; | |
} | |
void eraseSub(int pos) // Apaga subarvore de pos, exceto pos | |
{ | |
developed[pos] = 0; | |
if (e[pos] != 0) {eraseSub(e[pos]); avail.insert(e[pos]);} | |
if (d[pos] != 0) {eraseSub(d[pos]); avail.insert(d[pos]);} | |
e[pos] = 0; | |
d[pos] = 0; | |
} | |
void refresh(int pos) // Atualiza o no pos | |
{ | |
if (pos == 0) {seg[pos] = 0; peso[pos] = 0; return;} | |
seg[pos] = min(seg[e[pos]] + peso[e[pos]], seg[d[pos]] + peso[d[pos]]); | |
} | |
void build(int pos, int nivel, int par) // funcao para construir a arvore | |
{ | |
eraseSub(pos); developed[pos] = 1; | |
if (nivel >= 21 || par >= N) return; | |
if (e[pos] == 0) {int aux = create(); e[pos] = aux;} | |
if (d[pos] == 0) {int aux = create(); d[pos] = aux;} | |
int expNivel = (1 << nivel); | |
vector<int> viz = {e[pos], d[pos]}; | |
pai[e[pos]] = pai[d[pos]] = pos; | |
for (int i = 0; expNivel*i + par < N; i++) | |
{ | |
int id = expNivel * i + par; | |
if (seq[id] <= nivel) continue; | |
int chosenViz = 0; | |
if (nivel+1 == seq[id]) chosenViz = ((id >> nivel)&1); | |
else chosenViz = !((id>>nivel)&1); | |
peso[viz[!chosenViz]]++; | |
} | |
if (peso[viz[0]] <= K) build(viz[0], nivel + 1, par + expNivel); | |
if (peso[viz[1]] <= K) build(viz[1], nivel + 1, par); | |
refresh(viz[0]); refresh(viz[1]); refresh(pos); | |
} | |
int update(int pos, int nivel, int x, int id, int val, bool &willBuild) //pos na seg, nivel na arvore, nivel que quero chegar, id na seq, val pra atualizar | |
{ | |
if (nivel == x || !developed[pos]) return pos; | |
vector<int> viz = {e[pos], d[pos]}; | |
int chosenViz = 0; | |
if (nivel+1 == x) chosenViz = (id&1); | |
else chosenViz = !(id&1); | |
peso[viz[!chosenViz]] += val; | |
if (!developed[viz[chosenViz]] && peso[viz[chosenViz]] <= K) willBuild = 1; | |
if (!developed[viz[!chosenViz]] && peso[viz[!chosenViz]] <= K) willBuild = 1; | |
return update(viz[chosenViz], nivel+1, x, id>>1, val, willBuild); | |
} | |
int main() | |
{ | |
ios_base::sync_with_stdio(false); | |
cin.tie(NULL); | |
// Crio dois nos iniciais, similar a Seg Dinamica | |
create(); create(); | |
cin >> N >> Q >> K; | |
seq.resize(N); | |
for (auto &x : seq) cin >> x; | |
for (auto &x : seq) x = min(x, 21); | |
build(1, 0, 0); | |
cout << (seg[1] <= K) << '\n'; | |
while (Q--) | |
{ | |
int i, X; | |
cin >> i >> X; --i; X = min(X, 21); | |
int pos = 1, id = i, par = 0, type = 0, niv = 0; | |
bool willBuild = false; // guarda se preciso refazer o build | |
for (int nivel = 0; nivel <= 21; nivel++, niv++) | |
{ | |
if (nivel == X || nivel == seq[i] || !developed[pos]) | |
{ | |
if (nivel == X) type = 1; | |
else if (nivel == seq[i]) type = 2; | |
break; | |
} | |
vector<int> viz = {e[pos], d[pos]}; | |
int chosenOld = 0, chosenNew = 0; | |
if (nivel+1 == X) chosenNew = (id&1); | |
else chosenNew = !(id&1); | |
if (nivel+1 == seq[i]) chosenOld = (id&1); | |
else chosenOld = !(id&1); | |
if (chosenNew == chosenOld) {pos = viz[chosenNew]; id >>= 1; par += (1-chosenNew)*(1 << nivel); continue;} | |
if (!developed[viz[chosenOld]] && (peso[viz[chosenOld]]+1) <= K){ willBuild = 1; break; } | |
if (!developed[viz[chosenNew]] && (peso[viz[chosenNew]]-1) <= K){ willBuild = 1; break; } | |
break; | |
} | |
int pos1 = update(pos, niv, seq[i], id, -1, willBuild); | |
int pos2 = update(pos, niv, X, id, +1, willBuild); | |
while (pos1 != pos) | |
{ | |
pos1 = pai[pos1]; | |
refresh(e[pos1]); refresh(d[pos1]); | |
refresh(pos1); | |
} | |
while (pos2 != pos) | |
{ | |
pos2 = pai[pos2]; | |
refresh(e[pos2]); refresh(d[pos2]); | |
refresh(pos2); | |
} | |
seq[i] = X; | |
// Dou rebuild se necessario | |
if (willBuild) build(pos, niv, par); | |
while (pos != 1) | |
{ | |
pos = pai[pos]; | |
refresh(e[pos]); refresh(d[pos]); | |
refresh(pos); | |
} | |
cout << (seg[1] <= K) << '\n'; | |
} | |
} |