Esta página é dedicada aos comentários da OBI 2024. Para se preparar para a OBI, confira o nosso Roteiro de Estudos, nele você vai encontrar um direcionamento completo de como estudar!
Construtora
Comentário por Henrique Vianna
Conhecimento prévio necessário:
Perceba, primeiramente, que podemos considerar um segmento de prédios de mesma altura como um único prédio. Da mesma forma, perceba que a seguinte estratégia é válida e minimiza a quantidade de fases necessárias para a construtora completar a obra: selecionar o menor prédio e aumentar a sua altura até que ele atinja a altura do menor prédio entre seus vizinhos, removê-lo do vetor e refazer o processo até que reste um único prédio (o maior deles). Podemos remover o menor justamente porque ele passaria a formar um segmento de mesma altura com seu vizinho. Esse processo pode ser facilmente simulado com um vector ou até mesmo recursivamente.
Deve-se ressaltar que, no caso desse problema, soluções também recebiam pontos. Além disso, é possível resolvê-lo em .
Segue o código:
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; | |
const int INF = 110; // Valor maior que todos os outros | |
int main() | |
{ | |
ios_base::sync_with_stdio(false); | |
cin.tie(NULL); | |
int n; cin >> n; | |
vector<int> a(n); | |
for(auto &x : a) cin >> x; | |
int resp = 0; | |
while(a.size() > 1) | |
{ | |
int mn = *min_element(a.begin(), a.end()); // Valor minimo em a | |
int mnPos = 0; | |
for(int i = 0; i < a.size(); i++) | |
{ | |
if(a[i] == mn) | |
{ | |
mnPos = i; | |
int direita = (i + 1 < a.size() ? a[i + 1] - a[i] : INF); | |
int esquerda = (i - 1 >= 0 ? a[i - 1] - a[i] : INF); | |
// Igualo menor valor ao menor de seus vizinhos | |
resp += min(direita, esquerda); | |
break; | |
} | |
} | |
// Removo o minimo do vetor | |
a.erase(a.begin() + mnPos); | |
} | |
cout << resp << '\n'; | |
} |
Retas
Comentário escrito por Fernando Gonçalves
Conhecimento prévio necessário:
Subtarefa 2:
Só precisamos checar se a intersecção das duas retas está no intervalo .
A equação da abscissa da intersecção de duas retas é , sendo as retas e .
Obs: Quando , não existe intersecção e, assim, a fórmula dá erro por divisão por 0.
Subtarefa 3:
A solução é a mesma que para , mas fazemos para cada par de retas.
Subtarefa 4: para todo
As primeiras retas não tem intersecção entre si (elas têm todas a mesma inclinação).
Portanto, posso fazer a mesma ideia de , mas só testo os pares com a última reta.
Subtarefa 5:
Só queremos ver quantas retas têm intersecção na abscissa .
Para tanto, podemos manter um map de frequências, indicando quantas retas existem em um na abscissa .
Passo por todas as retas, quando estou na -ésima, somo na resposta e, em seguida, adiciono um na frequência desse y.
Subtarefa 6: e para todo .
Podemos fixar quais os das duas retas que vamos testar.
Com isso, sabemos que as retas devem ter , o que é um intervalo fixo para cada par de valores de . Obs: É preciso garantir que .
Podemos, então, calcular essa quantidade por meio de uma line sweep e um two pointers, para cada par de valores de .
Subtarefa 7: Sem restrições adicionais
Existem quatro casos da intersecção de duas retas: elas se encontram antes de , elas se encontram depois de , elas não se encontram, elas se encontram entre e .
Note que somente no caso de intersecção no meio a ordem relativa dos dessas retas muda da abscissa para a abscissa .
Dessa forma, só precisamos ordenar as retas pelo delas em e checar a quantidade de inversões na ordenação em , esta será a nossa resposta.
Obs: existe corner quando a intersecção ocorre em ou em , que pode ser corrigido por desempate na ordenação das retas.
Clique aqui para conferir o código.
Comentário por Murilo Maeda Kataoka
Conhecimento prévio necessário
- Binary Lifting
OBS: perceba que e que sempre podemos pegar o ancestral níveis acima
Subtarefa 2: A estrutura forma uma linha
Para essa subtarefa podemos perceber que, em qualquer momento, se soubermos o pai de um certo vértice , podemos facilmente calcular qual é o vértice que está níveis acima dele. Isso porque se algum vértice ainda possui filhos, com certeza, nenhum vértice acima dele sofreu reestruturação. Sabendo disso, podemos calcular facilmente o vértice que está posições acima de , já que o vértice com profundidade tem índice , pois . Então, o vértice que está posições acima de é o vértice . Consequentemente, para um vértice , quem está níveis acima dele será o vértice .
Agora, basta encontrar uma maneira de encontrar rapidamente o pai de um dado vértice . Para fazer isso, vamos manter qual foi o menor índice que sofreu uma operação e um vetor que guarda o pai original dos vértices. Só precisamos saber do menor número porque as operações feitas em vértices de maior índice não importam, uma vez que o vértice menor é ancestral desses vértices maiores e, por isso, agora são todos filhos diretos do vértice menor. Seja o menor número que sofreu reestruturação. Assim, em qualquer momento, para saber o pai de um determinado vértice , caímos em dois casos:
- i é menor que u ou ainda não houve reestruturação: nesse caso, a resposta é .
- i é maior que u: nesse caso, a resposta é .
A complexidade fica
Subtarefa 3:
Nessa parcial, para uma reestruturação no vértice , podemos simplesmente passar na subárvore de e setar que o pai desses vértices na subárvore é . Precisamos também criar um vetor de marcação, onde marcamos todos os vértices que possuem algum ancestral que sofreu reestruturação. Fazemos isso porque se a operação de reestruturação for em algum vértice que está marcado no vetor de marcação, não fazemos nada, já que ele não possuirá nenhum filho.
Agora, para saber quem está posições acima de um certo vértice , vamos utilizar a mesma observação da parcial anterior, de que se um vértice ainda possui filhos, nenhum ancestral dele sofreu reestruturação. Então, podemos simplesmente subir ancestrais fazendo , onde pegamos o pai vezes.
A complexidade fica .
Subtarefa 4: A estrutura forma uma árvore binária completa
Como a estrutura é uma árvore binária completa e , a sua principal característica é que a sua altura (ou profundidade) será no máximo , já que há vértices que possuem profundidade . E isso nos ajuda porque , ou .
Nesse caso, quando fizermos uma reestruturação no vértice , podemos simplesmente utilizar um vetor de marcação para indicar que houve reestruturação nesse vértice. Vou mostrar a seguir o porquê disso bastar para essa parcial.
Quando quisermos pegar o ancestral níveis acima de um vértice , novamente queremos o ancestral de menor índice que sofreu reestruturação, pois temos a observação da subtask 1. Para achar esse ancestral, podemos simplesmente iterar por todos os ancestrais de e pegar o ancestral de menor índice que está marcado no vetor de marcação. Então, a partir desse ancestral que achamos, basta subir níveis e acharemos a resposta. Se não acharmos nenhum ancestral que esteja marcado, subimos níveis a partir de .
A complexidade fica para reestruturações e para achar o superior níveis acima.
Subtarefa 5: Os nobres só têm que entregar relatórios para seus superiores diretos (pai)
Nesse subtask, basta saber o pai atual do vértice para responder as perguntas de entrega de relatório.
Agora, precisamos saber encontrar esse pai. Para fazer isso, basta saber para cada vértice qual é o ancestral de menor índice que sofreu alguma reestruturação (já que esse ancestral será seu pai atual). A fim de encontrar isso, podemos perceber que, quando fazemos reestruturação em um certo vértice , que não possui nenhum ancestral que sofreu reestruturação, todos os vértices na sua subárvore irão ter ele como ancestral de menor índice que sofreu reestruturação e como pai.
Então, queremos uma estrutura que faça as seguintes operações:
- Retorne para um vértice qual é o ancestral de menor índice que sofre reestruturação (se nenhum ancestral tiver sofrido reestruturação, retorna o pai na árvore original).
- Atualize para todos os vértices na subárvore de um vértice que é o ancestral de menor índice que sofreu reestruturação (se já tiver um ancestral que sofreu reestruturação, ele não terá nenhum filho, então, nesse caso, essa atualização não deve fazer nada).
Para fazer essa estrutura, vamos primeiro linearizar a árvore utilizando Euler Tour, que está descrito na subtask 3 desse problema.
Código do euler tour:
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
int timer, tin[MAXN], tout[MAXN]; | |
void dfs(int u, int p) | |
{ | |
tin[u] = ++timer; | |
for(auto v : adj[u]) | |
{ | |
if(v == p) continue; | |
dfs(v, u); | |
} | |
tout[u] = timer; | |
} | |
Agora, temos uma ordenação dos vértices que permite encontrar todos os vértices na subárvore de facilmente, já que todos os vértices tais que estão na subárvore de .
Observação importante: Se todos os vértices de uma subárvore, representada pelo intervalo de , possuem o mesmo pai depois de uma reestruturação nesse vértice, para todas as operações de reestruturação subsequentes, eles terão o mesmo pai, isto é, . Note que, no futuro, pode ser que esse pai em comum não seja , e sim um outro vértice que era ancestral de e sofreu reestruturação. Essa observação é verdadeira porque todas as reestruturações que mudam quem é o pai de algum vértice no intervalo em questão devem ter sido feitas em alguém que é ancestral de e, como a subárvore de está contida na subárvore de todos os seus ancestrais, todos os vértices na subárvore de terão como novo pai o ancestral de que foi reestruturado.
Com isso, podemos fazer uma segment tree no vetor da árvore linearizada (isto é, o índice nesse vetor representa o vértice que possui igual a e fazemos uma seg nesse vetor). Mas, como estamos querendo fazer updates em um intervalo e queries em um índice específico, essa seg será um pouco diferente.
Cada nó da seg cai em dois casos:
- Caso 1: Todos os vértices cujos tin estão no intervalo representado por possuem o mesmo ancestral de menor índice que sofreu reestruturação. Nesse caso, .
- Caso 2: Os vértices no intervalo de tin de NÃO possuem o mesmo ancestral de menor índice que sofreu reestruturação. Nesse caso, .
Utilizando a observação feita logo acima, fica fácil perceber que se todos os vértices no intervalo de de um nó da seg tiverem o mesmo ancestral que sofreu reestruturação, eles sempre terão o mesmo ancestral para as operações seguintes. Por isso, podemos setar . Por outro lado, se eles não possuem esse ancestral em comum, ainda não podemos afirmar nada sobre os seus pais atuais só com o vértice . É necessário, então, descer para fazer a busca nos filhos de .
Observe a situação inicial para maior clareza.
O update na seg será da seguinte forma:
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
//[ini,fim] é o intervalo representado pelo nó "pos" da seg | |
//queremos setar o intervalo [p,q] para ter pai igual a "val" | |
void update(int pos, int ini, int fim, int p, int q, int val) | |
{ | |
//se o [ini,fim] e [p,q] são disjuntos, não faz nada | |
if(p > fim || q < ini) return; | |
//se [ini,fim] está inteiramente contido no intervalo [p,q] | |
if(p <= ini && fim <= q) | |
{ | |
//com a observação, podemos só setar seg[pos] = val | |
//não é necessário descer para os filhos de pos | |
seg[pos] = min(seg[pos],val); | |
return; | |
} | |
int m = (ini + fim)/2; | |
int e = pos*2, d = pos*2 + 1; | |
//se não está inteiramente contido, desce até achar um que esteja inteiramente contido em [p,q]. | |
update(e,ini,m,p,q,val); | |
update(d,m+1,fim,p,q,val); | |
} |
Perceba que temos que fazer para evitar que ocorram reestruturações que, por exemplo, reestruturem um vértice e depois um filho de . Como , fazer faz com que essa segunda atualização não faça nada, que é o esperado.
A query será dessa forma:
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
//o nó "pos" representa o intervalo de tin [ini,fim] | |
//queremos saber o pai do vértice i tal que tin[i] = id | |
int query(int pos, int ini, int fim, int id) | |
{ | |
//se id não está em [ini,fim], retorna que é inválido (é bom ter, mesmo que isso nunca aconteça) | |
if(id > fim || id < ini) return -1; | |
//se for diferente de INF, todos os filhos tem o mesmo pai (seg[pos]) | |
//como id já está em [ini,fim], só retorna seg[pos] | |
if(ini == fim || seg[pos] != INF) | |
return seg[pos]; | |
int m = (ini + fim)/2; | |
int e = pos*2, d = pos*2 + 1; | |
//se o id que estamos buscando está na esquerda, retorna o da esquerda | |
//caso contrário retorna o da direita | |
if(id <= m) return query(e,ini,m,id); | |
else return query(d,m+1,fim,id); | |
} |
Para ficar mais claro, segue um exemplo de reestruturação:
Após reestruturação no vértice 5 (o update na seg fará com que os vértices com no intervalo tenham como ancestral de menor índice que sofreu reestruturação o vértice 5).
As queries e os updates ficam .
Subtask 6: sem restrições adicionais
Agora que já sabemos como fazer reestruturações rapidamente, basta descobrir como responder quem é o superior posições acima de um certo vértice rapidamente.
Para fazer isso utilizamos binary lifting! Para fazer o binary lifting, computamos a normal, que guarda, para um , qual é o vértice que está posições acima dele. Agora que temos isso, para saber quem está níveis acima de , basta fazer , para todo tal que e tenham um bit ativo em comum na notação binária desses números. Note que é o mesmo processo feito para igualar a profundidade de e no algoritmo de lca.
Observação importante: Se um certo vértice possui filhos, nenhum ancestral seu sofreu reestruturação e são todos iguais aos da árvore original. Isso é porque, se algum deles tivesse sofrido reestruturação, todos os filhos de seriam agora desse ancestral de que foi reestruturado.
Como sabemos o pai atual de um certo vértice (já que temos a seg descrita na parcial anterior), basta subir posições a partir do seu pai utilizando binary lifting. Como possui no máximo bits, a complexidade de uma operação desse tipo é .
Segue o código para ficar mais claro:
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> | |
#define INF 0x3f3f3f3f | |
using namespace std; | |
const int MAXN = 1e5 + 10; | |
const int LOG = 22; | |
int seg[MAXN*4],marc[MAXN*4], dp[MAXN][LOG], tin[MAXN], tout[MAXN]; | |
int N,temp = 0; | |
vector<int> grafo[MAXN]; | |
//Lineariza a árvore | |
void setup(int v, int p) | |
{ | |
tin[v] = ++temp; | |
for(int viz : grafo[v]) | |
{ | |
if(viz == p) | |
continue; | |
setup(viz,v); | |
} | |
tout[v] = temp; | |
} | |
//[ini,fim] é o intervalo representado pelo nó "pos" da seg | |
//queremos setar o intervalo [p,q] para ter pai igual a "val" | |
void update(int pos, int ini, int fim, int p, int q, int val) | |
{ | |
//se o [ini,fim] e [p,q] são disjuntos, não faz nada | |
if(p > fim || q < ini) return; | |
//se [ini,fim] está inteiramente contido no intervalo [p,q] | |
if(p <= ini && fim <= q) | |
{ | |
//com a observação, podemos só setar seg[pos] = val | |
//não é necessário descer para os filhos de pos | |
seg[pos] = min(seg[pos],val); | |
return; | |
} | |
int m = (ini + fim)/2; | |
int e = pos*2, d = pos*2 + 1; | |
//se não está inteiramente contido, desce até achar um que esteja inteiramente contido em [p,q]. | |
update(e,ini,m,p,q,val); | |
update(d,m+1,fim,p,q,val); | |
} | |
//o nó "pos" representa o intervalo de tin [ini,fim] | |
//queremos saber o pai do vértice i tal que tin[i] = id | |
int query(int pos, int ini, int fim, int id) | |
{ | |
//se id não está em [ini,fim], retorna que é inválido (é bom ter, mesmo que isso nunca aconteça) | |
if(id > fim || id < ini) return -1; | |
//se for diferente de INF, todos os filhos tem o mesmo pai (seg[pos]) | |
//como id já está em [ini,fim], só retorna seg[pos] | |
if(ini == fim || seg[pos] != INF) | |
return seg[pos]; | |
int m = (ini + fim)/2; | |
int e = pos*2, d = pos*2 + 1; | |
//se o id que estamos buscando está na esquerda, retorna o da esquerda | |
//caso contrário retorna o da direita | |
if(id <= m) return query(e,ini,m,id); | |
else return query(d,m+1,fim,id); | |
} | |
//Responde quem está k posições acima do vértice v | |
int responde(int v, int k) | |
{ | |
//acha o pai de v | |
int paiCur = query(1,1,N,tin[v]); | |
//Faz o binary lifting para ver quem está k - 1 posições acima do pai de v | |
k--; | |
for(int i = 0; i < LOG; i++) | |
{ | |
if(k & (1 << i)) | |
{ | |
paiCur = dp[paiCur][i]; | |
} | |
} | |
return paiCur; | |
} | |
int main() | |
{ | |
cin.tie(0)->sync_with_stdio(0); | |
cin >> N; | |
//Lê o grafo e inicializa a dp do binary lifting | |
dp[1][0] = 1; | |
for(int i = 2; i <= N; i++) | |
{ | |
cin >> dp[i][0]; | |
grafo[dp[i][0]].push_back(i); | |
} | |
setup(1,1); | |
//Calcula a dp do binary lifting | |
for(int i = 1; i < LOG; i++) | |
{ | |
for(int j = 1; j <= N; j++) | |
{ | |
dp[j][i] = dp[dp[j][i-1]][i-1]; | |
} | |
} | |
//Inicializa todo nó da seg como INF | |
for(int i = 1; i <= 4*N; i++) | |
{ | |
seg[i] = INF; | |
} | |
//Inicializa as folhas da seg como os os pais dos vértices na árvore inicial | |
for(int i = 1; i <= N; i++) | |
{ | |
update(1,1,N,tin[i],tin[i],dp[i][0]); | |
} | |
int Q; cin >> Q; | |
while(Q--) | |
{ | |
int type; cin >> type; | |
if(type == 1) | |
{ | |
int v,k; cin >> v >> k; | |
cout << responde(v,k) << "\n"; | |
} | |
else | |
{ | |
int v; cin >> v; | |
//Veja que o update é de tin[v] + 1 até tout[v] e não de tin[v] até tout[v] | |
//Isso porque não podemos setar que o pai de v é o próprio v | |
update(1,1,N,tin[v] + 1,tout[v],v); | |
} | |
} | |
} |
Brigadeiros
Comentário escrito por João Pedro Castro
Conhecimento prévio necessário:
- Introdução à Programação Dinâmica
- Problema da Mochila - Tecnicamente opcional, mas meio impossível fazer sem ter as ideias daqui.
Subtarefa 2: , ,
Vamos brutar tudo (testar todas as possibilidades de algo). Primeiro, vamos brutar as posições finais, isso é . Agora, para cada uma dessas possibilidades vamos brutar a ordem que cada um vai (se o primeiro menor das posições iniciais vai com a segunda menor das posições finais, etc), isso é . Por fim, para cada um desses casos conseguimos calcular em (mas até passaria) o mínimo para chegar nessa posição, e se falamos que a resposta é o máximo entre a resposta atual e a soma da quantidade de brigadeiros nas posições finais. A complexidade final é .
Dica de implementação: para brutar todas as possibilidades, você pode criar um vetor de posições que comece com 0s e depois tenha 1s. E então, usar next_permutation para ver todas as possibilidades. Em uma dada permutação desse vetor original você pode dizer que se e somente se a i-ésima posição for igual a 1 tem alguém do grupo.
Subtarefa 3: ,
Podemos fazer a mesma estratégia da parcial anterior mas com uma observação a mais: sejam as posições iniciais do grupo , e as finais : para conseguir o tempo mínimo de chegar nessa posição é ideal dizer que o cara na posição vai para a posição para todo ; ou seja, o menor vai com o menor, o segundo menor vai com o segundo menor, etc. A intuição disso é bem simples, e não é difícil "provar" (sem muita formalidade) fazendo umas contas.
Assim, conseguimos só brutar as possibilidades de escolhas, e calcular o tempo para chegar lá em . A complexidade final é .
Dica: o caso onde é maior é quando , então nesse caso o pior é , que ainda passa tranquilamente.
Subtarefa 4 e 5: ,
Vamos usar programação dinâmica! Aqui a definição de cada parte:
- Estado: nosso estado vai ser , onde:
- significa que usamos as posições de 1 até para as posições finais dos membros do grupo que colocamos até agora.
- significa que já colocamos os primeiros membros do grupo.
- significa o tempo máximo que podemos usar para chegar na melhor soma possível.
- significa a melhor soma possível que conseguimos dado , e .
- Caso base: o único caso base necessário é . O resto, se não calcularmos, pode ser visto só como .
- Transição: vou colocar várias possibilidades, e vai ser só o máximo entre elas.
- Podemos só não botar nada em : .
- Podemos ficar parados sem fazer nada: .
- Se , podemos botar o -ésimo (do menor para o maior em posição) membro do grupo na posição : (perceba que essa transição só funciona por conta da nossa observação que é sempre melhor colocar o menor com menor, segundo menor com segundo menor, etc.)
- A resposta, dado o estado, é .
A complexidade é .
Subtarefa 6:
Vamos fazer a mesma DP, só que com uma observação a mais. Responda essa pergunta: se posso fazer trocas de elementos adjacentes, qual o número máximo de swaps para ordenar o vetor?
Se você já viu a técnica de bubble sort, essa pergunta tem uma resposta bem direta: é , e na verdade sempre vai ser menor que (não convém mostrar a conta aqui). Logo, posso chegar de qualquer ordenação do vetor para qualquer outra em menos de operações, ou seja, logo após receber o podemos dizer que e a resposta ainda estaria certa, pois se existe uma resposta válida fazendo mais que trocas, também existirá uma fazendo menos que trocas.
Assim, tendo que , temos uma complexidade de .
Subtarefa 7: Sem restrições adicionais.
Quando você leu o enunciado, idealmente se atentou de um detalhe específico que não seria colocado sem motivo: . Ou seja, todo prato tem entre e brigadeiros, mas o que isso faz a gente pensar? Se você já está acostumado com problemas de DP, é direto: a soma máxima é .
E uma técnica legal de problemas de DP, é que em diversos problemas se você tem algo como (ou em um caso geral com quantas variáveis você quiser), você pode dizer que , ou qualquer outra troca. Ou seja, se você tem variáveis que indicam seu estado, e mais que indica sua resposta, você pode trocar qualquer uma das que indicam o estado com a da resposta que o estado ainda vai ser perfeitamente expressado. Claro que varia de problema a problema, e vai ter trocas melhores que outras, mas em problemas de DP sempre pense em qual a maneira com menos possibilidades de guardar esse estado.
No nosso caso, ao invés de fazer a , vamos fazer a . Já que o máximo de é e o máximo de é . Agora a DP significa: colocando os primeiros do grupo em algumas das primeiras posições, qual o menor tempo para atingir a soma ? Se for impossível podemos só dizer que .
O estado foi explicado acima, o caso base é o mesmo, e o resto fica assim:
- Transição: de novo, colocarei várias coisas aqui, e agora só pegue o mínimo entre elas.
- Não colocamos nada em : .
- Colocamos o -ésimo do grupo em : .
- A resposta vai ser o maior tal que existam quaisuer e com .
Temos estados, cada transição é , logo a complexidade final é .
OBS: A complexidade acaba ficando apertada, então para ficar tranquilo você tem que fazer pequenas "otimizações" (basicamente não passar por estados impossíveis), veja o código abaixo para ver as que fiz.
Clique aqui para conferir o código.
Jogo de Pratos
Comentário escrito por Fernando Gonçalves
Conhecimento prévio necessário:
Esse problema têm duas partes que trabalharemos separadamente: a parte dos feitiços e a das refeições. Neste comentário, será discutida a ideia geral do problema e, ao final, haverá discussão das parciais.
Parte dos feitiços
Temos equações de reta. Podemos usar cada uma delas quantas vezes quisermos e em qualquer ordem, contanto que sejam usadas exatamente delas no total. Queremos achar, dado um inicial, qual o máximo valor que podemos atingir.
Para tanto, usaremos, a todo momento, a equação que mais aumenta o nosso , só precisamos agora achar qual essa reta para toda operação.
Todas as retas são estritamente crescentes (é garantido pelo enunciado).
Assim, se eu tiver um maior, consigo um resultado maior em todas as retas.
Portanto, uma estratégia que não maximiza o após a operação nunca é ótima.
Utilizando o Convex Hull Trick, conseguimos achar a reta ótima para cada intervalo de . Dessa forma, conseguimos, por meio de uma busca binária nesse convex hull de retas, achar em a reta ótima para um qualquer. Isso nos dá uma complexidade de por query.
Estamos, contudo, fazendo algumas operações desnecessárias.
Note que, quando a reta ótima for aquela com maior do convex hull, todas as operações restantes serão feitas nesta reta. Se sobraram operações, o final () será (caso ) ou (caso ).
Além disso, no máximo uma operação será usada em qualquer outra reta.
Sendo as retas do convex hull , , ..., , com para todo , temos que (uma reta "dominada" por outra nunca é ótima).
Quando usamos uma reta , nenhuma outra reta menor que será usada daqui para frente. Portanto, suponha sem perda de generalidade que usamos a reta .
Após essa operação, temos que para todo .
Suponha, por contradição, que agora usaremos a reta ().
Sabemos, assim, que (pois é ótima) .
Entretanto, e e . Assim, , o que é uma contradição.
Dessarte, depois de uma operação, somente a última reta será usada.
Desta forma, só precisamos achar a reta ótima para o inicial, aplicar uma operação com esta, e aplicar todas as outras com a última reta. A complexidade disso é por query, ou seja, no total.
Parte das refeições
Temos equações de reta. Precisamos usar cada uma delas exatamente uma vez, mas podemos escolher a ordem. Essa ordem, portanto, será alguma permutação de tamanho .
Note que, para um dado , o resultado de todas as operações será . Assim, temos que , ou seja, a ordem ótima independe do .
Dessa forma, só precisamos calcular o maior somatório possível.
Intuitivamente, uma solução gulosa deveria funcionar (afinal, achar o maior valor de uma soma de produtos é um problema classicamente guloso), contudo a ordenação não é óbvia. Usaremos, então, um argumento da troca para achar essa ordenação ótima.
Suponha que temos uma ordem qualquer tal que, quando trocamos a ordem de e , o somatório aumenta. Como o prefixo e o sufixo das duas ordens é igual, temos que . Dessa forma, excetuando-se o caso em que um dos é (estes sempre estão no prefixo da ordem), a troca acontece quando e, portanto, nossa ordem gulosa é a ordenação por maior .
Tanto esse somatório quanto o produto dos podem ser calculados no começo do código, e sua conta calculada ao final de cada query. Resolvemos, então, essa parte em
Resumo da solução
Começamos o código ordenando as operações do segundo por maior (Cuidado: os devem ser colocados no prefixo).
Então, calculamos tanto o produto dos dessas operações quanto o somatório provenientes desse ordenamento. Salvaremos esses números para calcular a resposta final em cada query.
Em seguida, ordenamos as operações do primeiro tipo por crescente, e calculamos o convex hull das retas. Podemos, também, calcular os intervalos de ótimo para cada reta, para facilitar a implementação futura, mas isso não é estritamente necessário.
Agora, começamos a lidar com as queries. Para cada uma, vamos usar uma busca binária (ou ternária, caso os intervalos de não tenham sido calculados) para encontrar a primeira reta usada. Depois disso, usamos todas as operações restantes na última reta do convex hull (que é aquela com maior ). Finalmente, aplicamos a operação do segundo tipo, calculando, com isso, a resposta.
A complexidade total desse código é .
Clique aqui para conferir o código.
Subtaks
Nem todas as subtaks do problema precisavam dessas observações e/ou desses algoritmos. Apresentaremos ideias gerais de cada uma delas abaixo.
- Subtarefa 2 (): Nessa parcial, somente a independência entre o e a ordem da segunda operação precisa ser notada: podemos só testar toda combinação de cada operação. Nota: É necessário fazer as partes separadamente para evitar overflow.
- Subtarefa 3 (, é garantido que a quantidade final não excede ): Aqui, não é preciso resolver a segunda parte do problema. Ademais, a primeira parte pode ser resolvida com a mesma ideia, mas sem o uso do convex hull. Além disso, soluções que contém erro no cálculo do módulo ou que tentam maximizar o resto da resposta ao invés da resposta em si passam aqui.
- Subtarefa 4 (): Ainda não é necessário resolver a segunda parte; a primeira parte pode ser resolvida apenas testando qual a melhor reta para cada operação.
- Subtarefa 5 (): A segunda parte precisa ser resolvida como descrito; a primeira parte pode ser resolvida apenas testando qual a melhor reta para cada operação.
- Subtarefa 6 (): A segunda parte precisa ser resolvida como descrito; todas as observações da primeira parte são necessárias, mas pode-se só testar a reta ótima para o primeiro , ao invés de implementar convex hull.
- Subtarefa 7 (Sem restrições adicionais): A solução descrita no comentário.